Open5

SageMaker Serverless Inferenceチュートリアル

イナミイナミ

線形モデル(trainあり)

準備

test.csv
4078000,1994
4107000,1995
4118000,1996
4183000,1997
4185000,1998
4032000,1999
4082000,2000
4001000,2001
3887000,2002
3753000,2003
3766000,2004
3710000,2005
3670000,2006
3672000,2007
3652000,2008
3502000,2009
3547000,2010
3583000,2011
3521000,2012
3595000,2013
3614000,2014
3612000,2015
3562000,2016
3671000,2017
3716000,2018
3879000,2019
3701000,2020

SageMakerのIAM Roleが触れるバケットに上のcsvファイルを配置する。

コード

ipynb
import sagemaker

s3_data='s3://mlops/test.csv'
train_data = sagemaker.inputs.TrainingInput(
    s3_data,
    distribution="FullyReplicated",
    content_type="text/csv",
    s3_data_type="S3Prefix",
    record_wrapping=None,
    compression=None,
)

import boto3
from sagemaker.image_uris import retrieve

# sagemaker.image_uris.retrieveを使用して、線形学習アルゴリズム(linear-learner)のコンテナイメージURIを取得
container = retrieve("linear-learner", boto3.Session().region_name, version="1")
sess = sagemaker.Session()
role = sagemaker.get_execution_role()

# 学習アルゴリズムを実行するEstimator(推定器)を作成
linear = sagemaker.estimator.Estimator(
    container,
    role,
    instance_count=1,
    instance_type="ml.m4.xlarge",
    output_path="s3://mlops/output",
    sagemaker_session=sess,
)

# 最低限必要なハイパーパラメータを設定
linear.set_hyperparameters(
    predictor_type="regressor",
    mini_batch_size=10,
)
%%time
# ↑学習にかかった時間を計測

from time import gmtime, strftime

# ジョブ名を設定
job_name = "aota-linear-learner-4-" + strftime("%H-%M-%S", gmtime())
print("Training job", job_name)

# Estimator(推定器)にデータを投入して学習させる
linear.fit(inputs={'train': train_data, 'validation': train_data}, job_name=job_name, wait=True, logs='All')

from sagemaker.serverless import ServerlessInferenceConfig

serverless_config = ServerlessInferenceConfig(
    memory_size_in_mb = 2048,
    max_concurrency = 5
)
serverless_predictor = linear.deploy(serverless_inference_config = serverless_config)
---------------------!
import boto3
import json

endpoint_name = serverless_predictor.endpoint
client = boto3.client('runtime.sagemaker')
response = client.invoke_endpoint(
    EndpointName = endpoint_name,
    ContentType = 'text/csv',
    Body=b'1994\n1996',
    Accept = 'application/json'
)
model_predictions = json.loads(response['Body'].read())
print(model_predictions)
出力
The endpoint attribute has been renamed in sagemaker>=2.
See: https://sagemaker.readthedocs.io/en/stable/v2.html for details.
{'predictions': [{'score': 4101428.0}, {'score': 4054612.0}]}

参考

イナミイナミ

HuggingFace(学習なし、組み込み推論使用)

コード

ipynb
import sagemaker
from sagemaker.huggingface import HuggingFaceModel

# sess = sagemaker.Session()
role = sagemaker.get_execution_role()

# Hub Model configuration. https://huggingface.co/models
hub = {
  'HF_MODEL_ID':'distilbert-base-uncased-distilled-squad', # model_id from hf.co/models
  'HF_TASK':'question-answering' # NLP task you want to use for predictions
}

# create Hugging Face Model Class
huggingface_model_rth = HuggingFaceModel(
   env=hub, # hugging face hub configuration
   role=role, # iam role with permissions to create an Endpoint
   transformers_version="4.17", # transformers version used
   pytorch_version="1.10", # pytorch version used
   py_version="py38", # python version of the DLC
)

# # deploy model to SageMaker Inference
# predictor_rth = huggingface_model_rth.deploy(
#    initial_instance_count=1,
#    instance_type="ml.g4dn.xlarge"
# )
from sagemaker.serverless import ServerlessInferenceConfig

serverless_config = ServerlessInferenceConfig(
    memory_size_in_mb = 2048,
    max_concurrency = 5
)
serverless_predictor = huggingface_model_rth.deploy(serverless_inference_config = serverless_config)
--------!
import boto3
import json

endpoint_name = serverless_predictor.endpoint
client = boto3.client('runtime.sagemaker')
response = client.invoke_endpoint(
    EndpointName = endpoint_name,
    ContentType = 'application/json',
    Body=json.dumps({
            "inputs": {
                "question": "What is used for inference?",
                "context": "My Name is Philipp and I live in Nuremberg. This model is used with sagemaker for inference."
                }
        }),
    Accept = 'application/json'
)
model_predictions = json.loads(response['Body'].read())
print(model_predictions)
出力
The endpoint attribute has been renamed in sagemaker>=2.
See: https://sagemaker.readthedocs.io/en/stable/v2.html for details.
{'score': 0.9987210035324097, 'start': 68, 'end': 77, 'answer': 'sagemaker'}

参考

イナミイナミ

HuggingFace(モデルファイル利用、自作推論)

準備

cl-tohoku/bert-base-japanese-v2を利用させていただく

git lfs install
git clone https://huggingface.co/cl-tohoku/bert-base-japanese-v2
cd bert-base-japanese-v2

mkdir code
### ここで推論コードを書く
vim code/inference.py
vim code/requirements.txt

### 推論コード書き終わったら
tar -zcvf model.tar.gz *
aws s3 cp model.tar.gz s3://mlops/tohoku_bert.tar.gz

コード

code/inference.py
import json
import logging
import numpy as np

from six import BytesIO
from transformers import AutoTokenizer, AutoModelForMaskedLM, pipeline


logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

JSON_CONTENT_TYPE = 'application/json'
XNPY_CONTENT_TYPE = 'application/x-npy'
CSV_CONTENT_TYPE  = 'text/csv'

def input_fn(request_body, content_type=JSON_CONTENT_TYPE):
    logger.info("START input_fn")
    logger.info(f"content_type: {content_type}")
    logger.info(f"request_body: {request_body}")
    logger.info(f"type: {type(request_body)}")
    if content_type == XNPY_CONTENT_TYPE:
        stream = BytesIO(request_body)
        input_data = np.load(stream)
    elif content_type == CSV_CONTENT_TYPE:
        request_body = request_body.encode("utf-8") if isinstance(request_body, str) else request_body
        input_data = np.loadtxt(BytesIO(request_body), delimiter=",")
    elif content_type == JSON_CONTENT_TYPE:
        input_data = json.loads(request_body)
    else:
        # TODO: content_typeに応じてデータ型変換
        logger.error(f"content_type invalid: {content_type}")
        input_data = {"errors": [f"content_type invalid: {content_type}"]}
    logger.info("END input_fn")

    return input_data

def model_fn(model_dir):
    model = AutoModelForMaskedLM.from_pretrained(model_dir)
    tokenizer = AutoTokenizer.from_pretrained(model_dir)

    return model, tokenizer

def predict_fn(data, model_and_tokenizer):
    logger.info("START predict_fn")
    logger.info(f"data: {data}")
    # destruct model and tokenizer
    model, tokenizer = model_and_tokenizer
    
    fill_mask = pipeline("fill-mask", model=model, tokenizer=tokenizer)
    sentences = data["inputs"]
    k = data["top_k"]
    
    _out = fill_mask(sentences, top_k=k)
    logger.info("END predict_fn")
    
    return [elm["sequence"].replace(' ','') for elm in _out]
code/requirements.txt
mecab-python3
fugashi[unidic-lite,unidic]
ipynb
#!/usr/bin/env python
# coding: utf-8

# In[1]:

from sagemaker.huggingface import HuggingFaceModel
import sagemaker

role = sagemaker.get_execution_role()

# create Hugging Face Model Class
huggingface_model = HuggingFaceModel(
    model_data='s3://mlops/tohoku_bert.tar.gz',
    transformers_version='4.17.0',
    pytorch_version='1.10.2',
    py_version='py38',
    role=role,
)


# In[3]:


from sagemaker.serverless import ServerlessInferenceConfig

serverless_config = ServerlessInferenceConfig(
    memory_size_in_mb = 1024*6,
    max_concurrency = 5
)
serverless_predictor = huggingface_model.deploy(serverless_inference_config = serverless_config)


# In[5]:


import boto3
import json

endpoint_name = serverless_predictor.endpoint
client = boto3.client('runtime.sagemaker')
response = client.invoke_endpoint(
    EndpointName = endpoint_name,
    ContentType = 'application/json',
    Body=json.dumps({"inputs": "東北大学で[MASK]の研究をしています。", "top_k": 100}),
    Accept = 'application/json'
)
model_predictions = json.loads(response['Body'].read())
model_predictions
出力
['東北大学で医学の研究をしています。',
 '東北大学でアニメーションの研究をしています。',
 '東北大学で数学の研究をしています。',
 '東北大学で化学の研究をしています。',
 '東北大学で植物の研究をしています。',
 '東北大学で哲学の研究をしています。',
 '東北大学で教育の研究をしています。',
 '東北大学で薬学の研究をしています。',
 '東北大学で歴史の研究をしています。',
 '東北大学で農学の研究をしています。',
 '東北大学で音楽の研究をしています。',
 '東北大学で建築の研究をしています。',
 '東北大学で恐竜の研究をしています。',
 '東北大学で多くの研究をしています。',
 '東北大学で博士の研究をしています。',
 '東北大学でコンピュータの研究をしています。',
 '東北大学で映画の研究をしています。',
 '東北大学でジャズの研究をしています。',
 '東北大学で動物の研究をしています。',
 '東北大学で医療の研究をしています。',
 '東北大学でロボットの研究をしています。',
 '東北大学でマーケティングの研究をしています。',
 '東北大学で以下の研究をしています。',
 '東北大学でデザインの研究をしています。',
 '東北大学で工学の研究をしています。',
 '東北大学で日本の研究をしています。',
 '東北大学でハーブの研究をしています。',
 '東北大学で文学の研究をしています。',
 '東北大学で同様の研究をしています。',
 '東北大学で科学の研究をしています。',
 '東北大学でSFの研究をしています。',
 '東北大学で英語の研究をしています。',
 '東北大学で魔法の研究をしています。',
 '東北大学でがんの研究をしています。',
 '東北大学でマルチメディアの研究をしています。',
 '東北大学でメディアの研究をしています。',
 '東北大学でワインの研究をしています。',
 '東北大学で昆虫の研究をしています。',
 '東北大学でソフトウェアの研究をしています。',
 '東北大学でアイヌの研究をしています。',
 '東北大学で食の研究をしています。',
 '東北大学で福祉の研究をしています。',
 '東北大学で癌の研究をしています。',
 '東北大学で演劇の研究をしています。',
 '東北大学で農業の研究をしています。',
 '東北大学で介護の研究をしています。',
 '東北大学で言語の研究をしています。',
 '東北大学でUFOの研究をしています。',
 '東北大学で気象の研究をしています。',
 '東北大学で地震の研究をしています。',
 '東北大学でまちづくりの研究をしています。',
 '東北大学で法律の研究をしています。',
 '東北大学で料理の研究をしています。',
 '東北大学でサイエンスの研究をしています。',
 '東北大学でコンピューターの研究をしています。',
 '東北大学で民法の研究をしています。',
 '東北大学でサッカーの研究をしています。',
 '東北大学でバスケットボールの研究をしています。',
 '東北大学で中国の研究をしています。',
 '東北大学でコミュニケーションの研究をしています。',
 '東北大学で漫画の研究をしています。',
 '東北大学でウイルスの研究をしています。',
 '東北大学でパソコンの研究をしています。',
 '東北大学でアニメの研究をしています。',
 '東北大学で語学の研究をしています。',
 '東北大学でインターネットの研究をしています。',
 '東北大学でゲームの研究をしています。',
 '東北大学で理科の研究をしています。',
 '東北大学で農薬の研究をしています。',
 '東北大学で生物の研究をしています。',
 '東北大学で野球の研究をしています。',
 '東北大学で鳥類の研究をしています。',
 '東北大学で宇宙の研究をしています。',
 '東北大学で禅の研究をしています。',
 '東北大学でビジネスの研究をしています。',
 '東北大学でVRの研究をしています。',
 '東北大学で物理の研究をしています。',
 '東北大学で電気の研究をしています。',
 '東北大学でスポーツの研究をしています。',
 '東北大学でマンガの研究をしています。',
 '東北大学で囲碁の研究をしています。',
 '東北大学でフルートの研究をしています。',
 '東北大学でプログラミングの研究をしています。',
 '東北大学で経営の研究をしています。',
 '東北大学で教員の研究をしています。',
 '東北大学でアートの研究をしています。',
 '東北大学で現在の研究をしています。',
 '東北大学で詩の研究をしています。',
 '東北大学で仏教の研究をしています。',
 '東北大学でラジオの研究をしています。',
 '東北大学で原発の研究をしています。',
 '東北大学で美術の研究をしています。',
 '東北大学で翻訳の研究をしています。',
 '東北大学でAIの研究をしています。',
 '東北大学で国語の研究をしています。',
 '東北大学で地域の研究をしています。',
 '東北大学でワクチンの研究をしています。',
 '東北大学で色彩の研究をしています。',
 '東北大学でチェスの研究をしています。',
 '東北大学で蝶の研究をしています。']

参考

イナミイナミ

推論LambdaへのプロキシLambda構築(Python CDK利用)

推論Lambdaを外部公開したいがAuthenticating Requests (AWS Signature Version 4)をクリアする必要があり、そのままパブリックにすることは難しい(できないと思われる)。

公開エンドポイントにするためプロキシとしてパブリックなLambdaを用意する。
パプリックが前提のためIAM認証を取り入れる。

置き換え対象のALBの月間リクエスト数が、1400万リクエストより多いか少ないかが、この構成を採用するコストメリットを測る上で重要なポイント
引用:ExternalのApplication Load Balancer(ALB)をAPIGateway+InternalのALBに置き換えてみた

とのことで(2022/12/2現在は3億まで1.29USDでかなり安いので)、今回はAPIGatewayのHTTP APIとLambdaを利用しコストカットしたい。
ただし、リソースポリシー(ACL)がHTTP APIはまだ未対応なので、一旦REST APIで対応した。

準備

SageMaker StudioのJupyterのターミナル自体で作業するにはnodeのバージョンが古いので、更新する

nodeのバージョンが古くaws-cdk入らない
bash-4.2$ npm install -g aws-cdk
/opt/conda/bin/cdk -> /opt/conda/lib/node_modules/aws-cdk/bin/cdk
npm WARN notsup Unsupported engine for aws-cdk@2.53.0: wanted: {"node":">= 14.15.0"} (current: {"node":"12.16.1","npm":"6.13.4"})
npm WARN notsup Not compatible with your version of node/npm: aws-cdk@2.53.0
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.3.2 (node_modules/aws-cdk/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.3.2: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})

+ aws-cdk@2.53.0
added 1 package from 1 contributor in 1.263s
実行コマンド早見
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

nvm install 16
nodeバージョン更新ログ
bash-4.2$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100 15037  100 15037    0     0  72995      0 --:--:-- --:--:-- --:--:-- 72995
=> nvm is already installed in /home/sagemaker-user/.nvm, trying to update using git
=> => Compressing and cleaning up git repository

=> Profile not found. Tried ~/.bashrc, ~/.bash_profile, ~/.zshrc, and ~/.profile.
=> Create one of them and run this script again
   OR
=> Append the following lines to the correct file yourself:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm

=> You currently have modules installed globally with `npm`. These will no
=> longer be linked to the active version of Node when you install a new node
=> with `nvm`; and they may (depending on how you construct your `$PATH`)
=> override the binaries of modules installed with `nvm`:

/opt/conda/lib
├── @amzn/sagemaker-ui-graphql-server@3.32.1
├── aws-cdk@2.53.0
=> If you wish to uninstall them at a later point (or re-install them under your
=> `nvm` Nodes), you can remove them from the system Node as follows:

     $ nvm use system
     $ npm uninstall -g a_module

=> Close and reopen your terminal to start using nvm or run the following to use it now:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm

bash-4.2$ export NVM_DIR="$HOME/.nvm"
bash-4.2$ [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

bash-4.2$ nvm install 16
Downloading and installing node v16.18.1...
Downloading https://nodejs.org/dist/v16.18.1/node-v16.18.1-linux-x64.tar.gz...
################################################################################# 100.0%
Computing checksum with sha256sum
Checksums matched!
Now using node v16.18.1 (npm v8.19.2)

コード

CDK V2で記述しているためネットの情報が少なく少々手を焼いた
パッケージの依存関係もあるので、後でリポジトリ作る予定

app.py
#!/usr/bin/env python3
import os

from aws_cdk import App
from cdk.cdk_stack import CdkStack


app = App()
stack = CdkStack(app, "CdkStack")
app.synth()           # 超重要なので忘れずに付ける(これがないと記述が反映されない可能性がある)
cdk/cdk_stack.py
from constructs import Construct
from aws_cdk import Stack, Duration
from aws_cdk.aws_lambda import Function, Runtime, Code
from aws_cdk.aws_apigateway import RestApi, LambdaIntegration, AuthorizationType
from aws_cdk.aws_iam import PolicyStatement


class CdkStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        fn = Function(
            self,
            'integration-proxy',       # リソース名
            runtime=Runtime.PYTHON_3_8,      # 利用ランタイム
            code=Code.from_asset('lambda'),  # ディレクトリ指定
            handler='index.handler',         # 実行handler (プログラム.関数)
            environment={},                  # 環境変数
            memory_size=2048,
            initial_policy=[
                PolicyStatement(
                    actions=["sagemaker:InvokeEndpoint*"],
                    resources=["*"],
                ),
            ]
        )

        api = RestApi(
            self,
            "integration-api-gateway",
        )

        items = api.root.add_resource("invoke")
        items.add_method(
            "GET",
            LambdaIntegration(
                fn,
                proxy=True,
                timeout=Duration.seconds(10),
            ),
            authorization_type=AuthorizationType.IAM,
        ) # GET /invoke
lambda/index.py
import boto3
import json
import logging

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)


def handler(event, context):
    logger.debug("START handler")
    logger.debug(f"event: {event}")
    logger.debug(f"context: {context}")
    
    client = boto3.client('runtime.sagemaker')
    
    _EndpointName = event['queryStringParameters'].pop('EndpointName')
    _Body = event['queryStringParameters']
    _Body['top_k'] = int(_Body['top_k'])

    response=client.invoke_endpoint(
        EndpointName=_EndpointName,
        ContentType='application/json',
        Body=json.dumps(_Body),
        Accept='application/json'
    )
    return_value = {
        "statusCode": 200,
        "body": response['Body'].read().decode('utf-8'),
    }
    logger.debug(f"return_value: {return_value}")
    logger.debug("END handler")

    return return_value

こちらの 参考サイト と同様に、Postman等でIAM認証が正常なことが確認できる。

参考