2
0

Bumblebee による画像分類を Amazon SageMaker Serverless でサーバレスサービス化する

Last updated at Posted at 2024-09-24

はじめに

以前、 AWS の Amazon SageMaker 上に Elixir で画像分類サービスを構築しました

SageMaker 上に構築する場合、通常の Web サーバーと同じように Phoenix フレームワークで構築可能です

また、画像分類を AWS Lambda でサーバレス化する記事も書いています

Lambda の場合、サーバレスの特殊な環境で実行するため、 FaasBase というモジュールを利用しました

サーバレス化することで、使用していない間のコスト抑制、アクセスピーク時の可用性向上に効果が期待できます

本記事では、基本的に通常の SageMaker と同じ実装で Phoenix を使いつつサーバレス化可能な SageMaker Serverless によるサーバレスサービスを Livebook から構築します

実装したノートブックはこちら

事前準備

AWS のアカウント、 IAM ユーザーの認証情報を用意してください

Docker のビルドができるよう、 Docker Desktop や Rancher Desktop 等をインストールしてください

また、 Livebook をインストールし、起動してください

コンテナのローカル起動

画像分類コンテナを AWS 上で動かす前にローカルでビルドしておきます

コンテナ定義

全てここに記載するのは大変なので、以下の GitHub リポジトリーを参照してください

基本的に以前の記事と同じく、 Bumblebee で画像分類を実行する Phoenix サーバーになっています
(※以前の記事からモジュール等を更新しています)

以前の記事から変わったところを2点特筆しておきます

Apple シリコン搭載 Mac でのビルド

以前の記事は古い Intel Mac でビルドしていましたが、今回は M2 Mac でビルドしました

Lambda の実行環境は Intel CPU なので、ビルド時にいくつか指定が必要です

...
WORKDIR /app

+ ENV ERL_FLAGS="+JPperf true"
ENV HOME="/app"
ENV MIX_ENV="dev"

RUN mix deps.get
...

参考:

Docker コマンドでビルドする場合は --platform linux/amd64 の指定が必要です

また、 docker-compose でビルドする場合は platform: linux/amd64 の指定が必要です

権限設定

詳細には分かっていませんが、通常の SageMaker だと問題ないのに、 SageMaker Serverless の場合のみ、実行時に権限不足のエラーが発生します
(おそらく、実行ユーザーが違うため)

そのため、ビルドディレクトリー配下の権限を追加する必要があります

...
RUN mix compile.phoenix

RUN chmod +x /app/serve
+ RUN chmod -R 777 /app/_build
...

Livebook からの AWS 環境構築

こちらも基本的に以前の記事と同じです

コードは記載しますが、前回と同じ解説は割愛します

セットアップ

ノートブックのセットアップセルで以下のコードを実行します

Mix.install([
  {:aws, "~> 1.0"},
  {:ex_aws, "~> 2.5"},
  {:ex_aws_sts, "~> 2.3"},
  {:ex_aws_s3, "~> 2.5"},
  {:req, "~> 0.5"},
  {:nx, "~> 0.8"},
  {:stb_image, "~> 0.6"},
  {:hackney, "~> 1.20"},
  {:sweet_xml, "~> 0.7"},
  {:kino, "~> 0.14"},
  {:flow, "~> 1.2"}
])

今回はサーバーレス化されて自動スケーリングする状態を確認するため、並列処理用の Flow をインストールしています

認証

テキスト入力を作り、 AWS の認証情報を入力します

access_key_id_input = Kino.Input.password("ACCESS_KEY_ID")
secret_access_key_input = Kino.Input.password("SECRET_ACCESS_KEY")
region_input = Kino.Input.text("REGION")

[
  access_key_id_input,
  secret_access_key_input,
  region_input
]
|> Kino.Layout.grid(columns: 3)

スクリーンショット 2024-09-22 17.51.42.png

client =
  AWS.Client.create(
    Kino.Input.read(access_key_id_input),
    Kino.Input.read(secret_access_key_input),
    Kino.Input.read(region_input)
  )
auth_config = [
  access_key_id: Kino.Input.read(access_key_id_input),
  secret_access_key: Kino.Input.read(secret_access_key_input),
  region: Kino.Input.read(region_input)
]

Kino.nothing()

アカウントID取得

入力した AWS 認証情報のアカウントIDを取得しておきます

account_id =
  ExAws.STS.get_caller_identity()
  |> ExAws.request!(auth_config)
  |> then(& &1.body.account)

ECRリポジトリー作成

リージョンを指定します

region = Kino.Input.read(region_input)

任意の名前で ECR リポジトリーを用意します

image = "sagemaker-phoenix"
fullname = "#{account_id}.dkr.ecr.#{region}.amazonaws.com/#{image}:latest"
client
|> AWS.ECR.create_repository(%{
  "repositoryName" => image
})
client
|> AWS.ECR.describe_repositories(%{})
client
|> AWS.ECR.set_repository_policy(%{
  "repositoryName" => image,
  "policyText" =>
    Jason.encode!(%{
      "Statement" => [
        %{
          "Sid" => "ECR202201051440",
          "Effect" => "Allow",
          "Principal" => %{
            "AWS" => ["*"]
          },
          "Action" => "ecr:*"
        }
      ]
    })
})

ECR リポジトリーへのイメージプッシュ

コンテナをビルドし、 ECR リポジトリーにプッシュします

token =
  client
  |> AWS.ECR.get_authorization_token(%{})
  |> elem(1)
  |> Map.get("authorizationData")
  |> Enum.at(0)
  |> Map.get("authorizationToken")
[username, password] =
  token
  |> Base.decode64!()
  |> String.split(":")
System.cmd(
  "docker",
  [
    "login",
    "--username",
    username,
    "--password",
    password,
    fullname
  ]
)
{result, 0} =
  System.cmd(
    "docker",
    [
      "build",
      "--platform",
      "linux/amd64",
      "-t",
      image,
      "/sagemaker/serve"
    ]
  )

Kino.Markdown.new(result)

ここで --platform linux/amd64 を指定することで、 Apple シリコンの Mac でも有効なビルドができます

{result, 0} =
  System.cmd(
    "docker",
    [
      "tag",
      image,
      fullname
    ]
  )
{result, 0} =
  System.cmd(
    "docker",
    [
      "push",
      fullname
    ]
  )

Kino.Markdown.new(result)
client
|> AWS.ECR.list_images(%{
  "repositoryName" => image
})
|> elem(1)

スクリーンショット 2022-12-18 23.50.44.png

imageTag が latest のイメージが1件できていれば OK です

S3へのモデルファイルアップロード

画像分類用のモデルファイルを圧縮して S3 にアップロードします

アップロード先の指定

ExAws.S3.list_buckets()
|> ExAws.request!(auth_config)
|> then(& &1.body.buckets)
|> Kino.DataTable.new()
bucket_name_input = Kino.Input.text("BUCKET_NAME")
bucket_name = Kino.Input.read(bucket_name_input)
model_prefix = "models/"

モデルファイルの圧縮

models_path = "/sagemaker/serve/models"

filenames =
  models_path
  |> File.ls!()
  |> Enum.map(fn filename ->
    {
      to_charlist(filename),
      to_charlist(Path.join(models_path, filename))
    }
  end)
tar_filename = "models.tar.gz"

:erl_tar.create(tar_filename, filenames, [:compressed])

アップロードの実行

tar_filename
|> ExAws.S3.Upload.stream_file()
|> ExAws.S3.upload(bucket_name, model_prefix <> tar_filename)
|> ExAws.request!(auth_config)
bucket_name
|> ExAws.S3.list_objects(prefix: model_prefix)
|> ExAws.request!(auth_config)
|> then(& &1.body.contents)
|> Kino.DataTable.new()

IAM ロールの定義

SageMaker 実行用の IAM ロールを定義します

Serverless でも権限を変更する必要はありません

client
|> AWS.IAM.create_role(%{
  "RoleName" => "sagemaker-phoenix-role",
  "AssumeRolePolicyDocument" =>
    Jason.encode!(%{
      "Statement" => [
        %{
          "Sid" => "STS202201051440",
          "Effect" => "Allow",
          "Principal" => %{
            "Service" => ["sagemaker.amazonaws.com"]
          },
          "Action" => "sts:AssumeRole"
        }
      ]
    })
})
client
|> AWS.IAM.create_policy(%{
  "PolicyName" => "sagemaker-phoenix-role-policy",
  "PolicyDocument" =>
    Jason.encode!(%{
      "Version" => "2012-10-17",
      "Statement" => [
        %{
          "Effect" => "Allow",
          "Action" => [
            "cloudwatch:PutMetricData",
            "logs:CreateLogStream",
            "logs:PutLogEvents",
            "logs:CreateLogGroup",
            "logs:DescribeLogStreams",
            "ecr:GetAuthorizationToken"
          ],
          "Resource" => ["*"]
        },
        %{
          "Effect" => "Allow",
          "Action" => [
            "s3:GetObject"
          ],
          "Resource" => [
            "arn:aws:s3:::#{bucket_name}/*"
          ]
        },
        %{
          "Effect" => "Allow",
          "Action" => [
            "ecr:BatchCheckLayerAvailability",
            "ecr:GetDownloadUrlForLayer",
            "ecr:BatchGetImage"
          ],
          "Resource" => [
            "arn:aws:ecr:::repository/#{image}"
          ]
        }
      ]
    })
})
client
|> AWS.IAM.get_policy_version(%{
  "PolicyArn" => "arn:aws:iam::#{account_id}:policy/sagemaker-phoenix-role-policy",
  "VersionId" => "v1"
})
|> elem(1)
|> then(& &1["GetPolicyVersionResponse"]["GetPolicyVersionResult"]["PolicyVersion"]["Document"])
|> URI.decode()
|> Jason.decode!()
client
|> AWS.IAM.attach_role_policy(%{
  "RoleName" => "sagemaker-phoenix-role",
  "PolicyArn" => "arn:aws:iam::#{account_id}:policy/sagemaker-phoenix-role-policy"
})

SageMaker モデル作成

モデル作成は通常時と変わりません

client
|> AWS.SageMaker.create_model(%{
  "ModelName" => "sagemaker-phoenix-model",
  "ExecutionRoleArn" => "arn:aws:iam::#{account_id}:role/sagemaker-phoenix-role",
  "PrimaryContainer" => %{
    "Image" => fullname,
    "ModelDataUrl" => "s3://#{bucket_name}/#{model_prefix}#{tar_filename}"
  }
})

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

エンドポイント設定が SageMaker Serverless で変更するポイントです

client
|> AWS.SageMaker.create_endpoint_config(%{
  "EndpointConfigName" => "sagemaker-phoenix-endpoint-config",
  "ProductionVariants" => [
    %{
      "VariantName" => "variant-1",
      "ModelName" => "sagemaker-phoenix-model",
      "ServerlessConfig" => %{ 
        "MaxConcurrency" => 4,
        "MemorySizeInMB" => 6144,
        "ProvisionedConcurrency" => 1
      }
    }
  ]
})

以下の ServerlessConfig を追加しするだけで SageMaker サーバレスに変更されます

InstanceType の指定は不要になります

...
      "ServerlessConfig" => %{ 
        "MaxConcurrency" => 4,
        "MemorySizeInMB" => 6144,
        "ProvisionedConcurrency" => 1
      }
...

InstanceTypeml.inf1 などのインスタンスタイプを指定していると、サーバレスに対応していない旨が表示されます

Inferentia や GPU インスタンスには対応していないため、その点は注意が必要です

SageMaker エンドポイント作成

client
|> AWS.SageMaker.create_endpoint(%{
  "EndpointName" => "sagemaker-phoenix-endpoint",
  "EndpointConfigName" => "sagemaker-phoenix-endpoint-config"
})

以下のコードの結果が InService になるまで待ちます

client
|> AWS.SageMaker.describe_endpoint(%{
  "EndpointName" => "sagemaker-phoenix-endpoint"
})
|> elem(1)
|> Map.get("EndpointStatus")

SageMaker エンドポイント呼出

単独画像

実際に画像を SageMaker に投げて画像分類を実行します

binary =
  "https://raw.githubusercontent.com/pjreddie/darknet/master/data/dog.jpg"
  |> Req.get!()
  |> Map.get(:body)
binary
|> StbImage.read_binary!()
|> StbImage.to_nx()
|> Kino.Image.new()

投げるのは以下の画像です

スクリーンショット 2022-12-19 0.25.00.png

client
|> AWS.SageMakerRuntime.invoke_endpoint(
  "sagemaker-phoenix-endpoint",
  %{
    "Body" => binary,
    "ContentType" => "image/jpeg",
    "Accept" => "application/json"
  },
  recv_timeout: 60_000
)
|> elem(1)
|> Map.get("Body")
|> Jason.decode!()
|> Map.get("predictions")
|> Kino.DataTable.new()

以下のように推論結果が返ってきます

スクリーンショット 2022-12-19 0.26.04.png

並列推論

Flow で並列実行してみましょう

0..10
|> Flow.from_enumerable(max_demand: 1)
|> Flow.map(fn _index ->
  client
  |> AWS.SageMakerRuntime.invoke_endpoint(
    "sagemaker-phoenix-endpoint",
    %{
      "Body" => binary,
      "ContentType" => "image/jpeg",
      "Accept" => "application/json"
    },
    recv_timeout: 60_000
  )
end)
|> Enum.to_list()

awslogs などでログ出力を確認すると、複数インスタンスが起動して並列分散処理されていることが分かります

variant-1/fac77384aafc5731b17cb634d80f4f3e-c29d16194858454caa496a95f294a13e などの部分が実際に動作しているインスタンスの ID です

複数 ID が出力されています

/aws/sagemaker/Endpoints/sagemaker-phoenix-endpoint variant-1/fac77384aafc5731b17cb634d80f4f3e-c29d16194858454caa496a95f294a13e [info] POST /invocations
/aws/sagemaker/Endpoints/sagemaker-phoenix-endpoint variant-1/fac77384aafc5731b17cb634d80f4f3e-c29d16194858454caa496a95f294a13e [debug] Processing with ApiWeb.PredictionController.index/2
/aws/sagemaker/Endpoints/sagemaker-phoenix-endpoint variant-1/fac77384aafc5731b17cb634d80f4f3e-c29d16194858454caa496a95f294a13e   Parameters: %{}
/aws/sagemaker/Endpoints/sagemaker-phoenix-endpoint variant-1/fac77384aafc5731b17cb634d80f4f3e-c29d16194858454caa496a95f294a13e   Pipelines: [:api]
/aws/sagemaker/Endpoints/sagemaker-phoenix-endpoint variant-1/94ed1e91bb2a18d06f327889ddb597b3-4c85239a484f4b008e52d3254d8c4d59 [info] Predictions: [%{label: "malamute, malemute, Alaskan malamute", score: 0.9850469827651978}, %{label: "bicycle-built-for-two, tandem bicycle, tandem", score: 0.0035503478720784187}, %{label: "mountain bike, all-terrain bike, off-roader", score: 0.0011567040346562862}, %{label: "Eskimo dog, husky", score: 7.718618726357818e-4}, %{label: "Tibetan mastiff", score: 5.428484291769564e-4}]
/aws/sagemaker/Endpoints/sagemaker-phoenix-endpoint variant-1/94ed1e91bb2a18d06f327889ddb597b3-4c85239a484f4b008e52d3254d8c4d59 [info] Sent 200 in 3574ms
/aws/sagemaker/Endpoints/sagemaker-phoenix-endpoint variant-1/8635afe3515d0a8fbfaa93dc1bc7358b-264ef0238c7c4fedb750356eb0281700 [info] Predictions: [%{label: "malamute, malemute, Alaskan malamute", score: 0.9850469827651978}, %{label: "bicycle-built-for-two, tandem bicycle, tandem", score: 0.0035503478720784187}, %{label: "mountain bike, all-terrain bike, off-roader", score: 0.0011567040346562862}, %{label: "Eskimo dog, husky", score: 7.718618726357818e-4}, %{label: "Tibetan mastiff", score: 5.428484291769564e-4}]
/aws/sagemaker/Endpoints/sagemaker-phoenix-endpoint variant-1/8635afe3515d0a8fbfaa93dc1bc7358b-264ef0238c7c4fedb750356eb0281700 [info] Sent 200 in 5674ms
/aws/sagemaker/Endpoints/sagemaker-phoenix-endpoint variant-1/fac77384aafc5731b17cb634d80f4f3e-c29d16194858454caa496a95f294a13e [info] Predictions: [%{label: "malamute, malemute, Alaskan malamute", score: 0.9850469827651978}, %{label: "bicycle-built-for-two, tandem bicycle, tandem", score: 0.0035503478720784187}, %{label: "mountain bike, all-terrain bike, off-roader", score: 0.0011567040346562862}, %{label: "Eskimo dog, husky", score: 7.718618726357818e-4}, %{label: "Tibetan mastiff", score: 5.428484291769564e-4}]
/aws/sagemaker/Endpoints/sagemaker-phoenix-endpoint variant-1/fac77384aafc5731b17cb634d80f4f3e-c29d16194858454caa496a95f294a13e [info] Sent 200 in 4758ms
/aws/sagemaker/Endpoints/sagemaker-phoenix-endpoint variant-1/d0a505828f0da4b94bc2f61a92e1e4ee-a851e8dbea4046488bf665a6965c7a13 [info] POST /invocations
/aws/sagemaker/Endpoints/sagemaker-phoenix-endpoint variant-1/d0a505828f0da4b94bc2f61a92e1e4ee-a851e8dbea4046488bf665a6965c7a13 [debug] Processing with ApiWeb.PredictionController.index/2
/aws/sagemaker/Endpoints/sagemaker-phoenix-endpoint variant-1/d0a505828f0da4b94bc2f61a92e1e4ee-a851e8dbea4046488bf665a6965c7a13   Parameters: %{}
/aws/sagemaker/Endpoints/sagemaker-phoenix-endpoint variant-1/d0a505828f0da4b94bc2f61a92e1e4ee-a851e8dbea4046488bf665a6965c7a13   Pipelines: [:api]
/aws/sagemaker/Endpoints/sagemaker-phoenix-endpoint variant-1/d0a505828f0da4b94bc2f61a92e1e4ee-a851e8dbea4046488bf665a6965c7a13 [info] Predictions: [%{label: "malamute, malemute, Alaskan malamute", score: 0.9850469827651978}, %{label: "bicycle-built-for-two, tandem bicycle, tandem", score: 0.0035503478720784187}, %{label: "mountain bike, all-terrain bike, off-roader", score: 0.0011567040346562862}, %{label: "Eskimo dog, husky", score: 7.718618726357818e-4}, %{label: "Tibetan mastiff", score: 5.428484291769564e-4}]
/aws/sagemaker/Endpoints/sagemaker-phoenix-endpoint variant-1/d0a505828f0da4b94bc2f61a92e1e4ee-a851e8dbea4046488bf665a6965c7a13 [info] Sent 200 in 371ms

後片付け

作成したリソースを削除します
(余計なコストを発生させないため)

SageMaker エンドポイント削除

client
|> AWS.SageMaker.delete_endpoint(%{
  "EndpointName" => "sagemaker-phoenix-endpoint"
})
client
|> AWS.SageMaker.delete_endpoint_config(%{
  "EndpointConfigName" => "sagemaker-phoenix-endpoint-config"
})
client
|> AWS.SageMaker.delete_model(%{
  "ModelName" => "sagemaker-phoenix-model"
})

IAM ロール削除

client
|> AWS.IAM.detach_role_policy(%{
  "RoleName" => "sagemaker-phoenix-role",
  "PolicyArn" => "arn:aws:iam::#{account_id}:policy/sagemaker-phoenix-role-policy"
})
client
|> AWS.IAM.delete_policy(%{
  "PolicyArn" => "arn:aws:iam::#{account_id}:policy/sagemaker-phoenix-role-policy"
})
client
|> AWS.IAM.delete_role(%{
  "RoleName" => "sagemaker-phoenix-role"
})

ECR リポジトリー削除

全てのイメージを削除してからリポジトリーを削除します

image_digest_list =
  client
  |> AWS.ECR.list_images(%{
    "repositoryName" => image
  })
  |> elem(1)
  |> Map.get("imageIds")
  |> Enum.map(&Map.take(&1,["imageDigest"]))
client
|> AWS.ECR.batch_delete_image(%{
  "repositoryName" => image,
  "imageIds" => image_digest_list
})
client
|> AWS.ECR.list_images(%{
  "repositoryName" => image
})
|> elem(1)
|> Map.get("imageIds")
|> Kino.DataTable.new()
client
|> AWS.ECR.delete_repository(%{
  "repositoryName" => image
})

モデルファイル削除

ExAws.S3.delete_object(bucket_name, model_prefix <> tar_filename)
|> ExAws.request!(auth_config)

まとめ

通常の SageMaker とほぼ同じ容量で SageMaker Serverless を使用することができました

特に呼出頻度が低い場合や、極端にアクセス集中する時間がある場合などに有効です

通常 SageMaker で動かしていたコンテナをほぼそのままサーバレス化できるのは素晴らしいですね

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0