Amazon SageMakerの推論エンドポイントでA/Bテストのためのバリアント追加・削除を試してみる

Amazon SageMakerの推論エンドポイントでA/Bテストのためのバリアント追加・削除を試してみる

バリアントを使うことで推論エンドポイント背後で複数のモデルをホストすることができます。
Clock Icon2025.03.28

データ事業本部の鈴木です。

推論エンドポイントではproduction variants(以降、バリアント)を使うことにより簡単に新旧のモデルをエンドポイントに並行してデプロイし、加重ルーティングなどの制御を行いつつ評価することができます。

今回はSageMaker Python SDKおよびboto3を使ってどのようにバリアントを追加・削除していくのか確認しました。

バリアントについて

推論エンドポイント背後でモデルをホストするためのインスタンスの定義をするためのものになります。

https://docs.aws.amazon.com/sagemaker/latest/dg/model-ab-testing.html

この仕組みを使うことにより、同一の推論エンドポイントの裏に異なるバージョンのモデルをデプロイできます。インスタンス数はスケーリング設定を行うことができます。

各インスタンスへのトラフィックの重み設定も容易に行えるほか、エンドポイントへのアクセス時にバリアントを明示的に指定することで特定のバリアントに対してアクセスすることもできます。

バリアントを使ったA/Bテストについて

バリアントを複数デプロイすることでA/Bテストを行うこともできます。多くの場合SageMaker 推論エンドポイントの前段にAPI Gateway + Lambdaなどの構成を配置するなどしてサービス提供することになると思います。ユーザーによって対応するバリアントを指定してアクセスするよう、APIを設計することになるでしょう。

A/Bテストについては、推論エンドポイントの機能だけにはなりますが、以下のガイド・ブログで例が紹介されています。

https://sagemaker-examples.readthedocs.io/en/latest/sagemaker_endpoints/a_b_testing/a_b_testing.html

https://aws.amazon.com/jp/blogs/news/a-b-testing-ml-models-in-production-using-amazon-sagemaker/

本記事も一つ目のリンクのガイドの実装を参考にしています。

エンドポイントへのアクセス時にバリアントを指定しない場合は、単一のバリアントしかデプロイされていないときはそのバリアントに、複数あるときは設定した重みに応じてランダムにルーティングされます。実装にもよりますが、A/Bテスト時以外はバリアントを指定しないようにしているなら、バリアント削除の際は新しいバリアントにしかルーティングしないようにしてから古いバリアントを削除するのが安全です。

やってみた

今回は想定される操作をSageMaker StudioのJupyterLabスペースのノートブックで、通して実施してみました。

イメージおよびライブラリは以下です。

  • SageMaker Distribution 2.4.1
  • boto3 1.36.23
  • SageMaker Python SDK 2.240.0

1. モデルのトレーニングと作成

以下のブログで紹介されているirisデータセット向けの分類モデルを2つ訓練しました。データの前準備までは内容が重複するため省略します。

https://dev.classmethod.jp/articles/sagemaker-core/

num_roundハイパーパラメータだけ少し値を変えました。

# 一つ目のモデル
from sagemaker.inputs import TrainingInput

estimator = sagemaker.estimator.Estimator(
    sagemaker_session=sagemaker_session,
    base_job_name="xgboost-iris",
    image_uri=image, 
    role=role,
    instance_count=1, 
    instance_type='ml.m4.xlarge', 
    volume_size=30,
    output_path=s3_output_path,
    max_run=600)

estimator.set_hyperparameters(
    objective="multi:softmax",
    num_class=3,
    num_round=10,
    eval_metric="merror")

train_input = TrainingInput(s3_input_path, content_type="csv")

estimator.fit({'train': train_input})

# 二つ目のモデル(処理内容自体は一つ目と全く同じ)
from sagemaker.inputs import TrainingInput

estimator_2 = sagemaker.estimator.Estimator(
    sagemaker_session=sagemaker_session,
    base_job_name="xgboost-iris2",
    image_uri=image, 
    role=role,
    instance_count=1, 
    instance_type='ml.m4.xlarge', 
    volume_size=30,
    output_path=s3_output_path,
    max_run=600)

# num_roundだけ変える
estimator_2.set_hyperparameters(
    objective="multi:softmax",
    num_class=3,
    num_round=15,
    eval_metric="merror")

train_input = TrainingInput(s3_input_path, content_type="csv")

estimator_2.fit({'train': train_input})

それぞれ、異なるモデルとしてSageMakerに登録しました。

from sagemaker.model import Model

model = Model(
    image_uri=image,
    model_data=estimator.model_data,
    name='iris-model-a',
    role=role)
model.create(
    instance_type="ml.m5.xlarge"
)

model2 = Model(
    image_uri=image,
    model_data=estimator_2.model_data,
    name='iris-model-b',
    role=role)
model2.create(
    instance_type="ml.m5.xlarge"
)

モデルを確認すると、指定した名前で2つモデルが作成されていました。

モデルの作成

2. バリアントの作成とエンドポイントのデプロイ

つづいて、モデルを指定した2つのバリアントを作成しました。ml.c5.4xlargeインスタンスを各バリアントに対して1つずつ作成します。

from sagemaker.session import production_variant

variant1 = production_variant(
    model_name=model.name,
    instance_type="ml.c5.4xlarge",
    initial_instance_count=1,
    variant_name="Variant1",
    initial_weight=1,
)
variant2 = production_variant(
    model_name=model2.name,
    instance_type="ml.c5.4xlarge",
    initial_instance_count=1,
    variant_name="Variant2",
    initial_weight=1,
)

バリアントよりエンドポイントを作成しました。

endpoint_name = "DEMO-xgb-iris-pred"
print(f"EndpointName={endpoint_name}")

sagemaker_session.endpoint_from_production_variants(
    name=endpoint_name, production_variants=[variant1, variant2]
)

実行すると、以下のようにStudioからエンドポイントを作成していることが確認できました。バリアントは2つ設定されています。

エンドポイントの作成中

なお、今回は先に紹介したガイドに沿ってバリアントから新規にエンドポイントを作成しましたが、既に運用中のエンドポイントで行う場合は、後ほど紹介する例と同じく、新しいエンドポイント設定を作成してupdate_endpointでエンドポイントを更新するのがよいと思います。

3. 推論の実行

エンドポイントを起動できたので、推論が実行できるか確認してみました。まずSageMaker Python SDKでバリンアントを指定して行っています。(指定しなければ加重ルーティングされます。)

from sagemaker.predictor import Predictor
from sagemaker.serializers import CSVSerializer

predictor = Predictor(endpoint_name=endpoint_name, serializer=CSVSerializer())
response = predictor.predict(data=test_data_no_target.values, target_variant="Variant1")
print("Endpoint Response:", response.decode("utf-8"))

response = predictor.predict(data=test_data_no_target.values, target_variant="Variant2")
print("Endpoint Response:", response.decode("utf-8"))

以下のように結果が返ってきました。

推論の実行

また、boto3からも推論できます。こちらはSageMaker Python SDKと異なり、どのバリアントで推論したかなどのレスポンスを取得できます。

sm_runtime = boto3.Session().client("sagemaker-runtime")
csv_text = '6.1 2.8 4.7 1.2'
response = sm_runtime.invoke_endpoint(
    EndpointName=endpoint_name,
    ContentType='text/csv',
    Body=csv_text
)
response['InvokedProductionVariant']
# 'Variant2'

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sagemaker-runtime/client/invoke_endpoint.html

推論エンドポイントのメトリクスはコンソールから確認することができました。

推論エンドポイントのメトリクス

4. エンドポイントの更新

バリアント2の方が結果がよいことを確認できた、としてエンドポイントにデプロイしているバリアントを2だけに更新しました。

まずはバリアントの重みをバリアント2が100%になるように更新しました。

import time
sm = boto_session.client("sagemaker")

sm.update_endpoint_weights_and_capacities(
    EndpointName=endpoint_name,
    DesiredWeightsAndCapacities=[
        {"DesiredWeight": 0, "VariantName": variant1["VariantName"]},
        {"DesiredWeight": 1, "VariantName": variant2["VariantName"]},
    ],
)
print("Waiting for update to complete")
while True:
    status = sm.describe_endpoint(EndpointName=endpoint_name)["EndpointStatus"]
    if status in ["InService", "Failed"]:
        print("Done")
        break
    print(".", end="", flush=True)
    time.sleep(1)

{
    variant["VariantName"]: variant["CurrentWeight"]
    for variant in sm.describe_endpoint(EndpointName=endpoint_name)["ProductionVariants"]
}

# Waiting for update to complete
# ....................................................................Done
# {'Variant1': 0.0, 'Variant2': 1.0}

コンソールから確認すると、エンドポイントは以下のようになっていました。

重み更新後のエンドポイント

なお、この状態でもVariant1での推論はバリアントを指定するとできました。バリアント指定しない場合のルーティングがされなくなっただけのようです。

続いて、不要になったVariant1を削除しました。これには、まず新しいエンドポイント設定を作成しました。

sm.create_endpoint_config(
    EndpointConfigName = 'DEMO-xgb-iris-pred-after-ab-testing',
    ProductionVariants = [
        {
            "VariantName": "Variant2",
            "InstanceType": "ml.c5.4xlarge",
            "InitialInstanceCount": 1,
            "InitialVariantWeight": 1,
            "ModelName": model2.name
        }
    ]
)

新しいエンドポイント設定

この設定で推論エンドポイントを更新しました。

sm.update_endpoint(
    EndpointName=endpoint_name,
    EndpointConfigName='DEMO-xgb-iris-pred-after-ab-testing'
)

補足

エンドポイントの削除

この記事を見ている方は検証目的が多いと思うので、エンドポイントの削除方法も記載します。
各種SDKから削除できますが、ここではboto3を使った削除を紹介します。エンドポイント名を指定して簡単に削除できます。

response = sm.delete_endpoint(
    EndpointName=endpoint_name
)

操作に使うSDKおよびAPI

今回紹介した操作をするのに、SDKが多く登場しました。ここは実際に手を動かしてみないと混乱しやすいです。
以下がありました。

複数バリアントを使ったモデルホスティングの位置付けについて

現状、Amazon SageMakerでは多数のモデルホスティングの選択肢があります。

  • シングルモデルエンドポイント
  • マルチバリアントエンドポイント
  • マルチモデルエンドポイント
  • マルチコンテナエンドポイント
  • 推論コンポーネントによるデプロイ

今回は2つ目の方法について説明しました。上4つの選択肢内でマルチバリアントエンドポイントがどういう立ち位置なのかについては、以下のブログに紹介がありました。

https://aws.amazon.com/jp/blogs/machine-learning/part-6-model-hosting-patterns-in-amazon-sagemaker-best-practices-in-testing-and-updating-models-on-sagemaker/

複数のモデルをホストできるという点はほかの複数モデルをホストするエンドポイントと同じですが、バリアントごとにインスタンスが起動するのは特徴かなと思いました。

また、マルチバリアントエンドポイントで運用する場合、デプロイが頻繁なケースではデプロイメントガードレールを使うように案内されていました。気になる方はご確認ください。

https://docs.aws.amazon.com/sagemaker/latest/dg/deployment-guardrails.html

最後に

推論エンドポイントでA/Bテストなどを想定してバリアントの追加・削除を行う例をご紹介しました。参考になりましたら幸いです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.