Lambda MicroVMsのライフサイクルフックで、MLモデル推論の初回待ち時間を短縮できるか試してみた

Lambda MicroVMsのライフサイクルフックで、MLモデル推論の初回待ち時間を短縮できるか試してみた

Lambda MicroVMsのビルド時ライフサイクルフック(/ready, /validate)を使い、MLモデル推論のコールドスタートをどこまで短縮できるか検証しました。フック構成3パターンの比較結果と実装時の注意点をまとめます。
2026.06.24

はじめに

2026年6月22日、AWS Lambdaの新しいコンピューティングプリミティブとして Lambda MicroVMs がリリースされました。

https://aws.amazon.com/jp/about-aws/whats-new/2026/06/aws-lambda-microvms/

前回の記事ではLambda MicroVMs上にFlaskアプリをデプロイし、サスペンド・レジュームの基本動作を確認しました。

https://dev.classmethod.jp/articles/aws-lambda-microvms-flask-suspend-resume/

フックなしのシンプルな構成でしたが、初期化に時間のかかるワークロード(MLモデルのロードなど)では、Run後の初回リクエストで長い待ち時間が発生します。

Lambda MicroVMsにはこの問題に対処するビルド時ライフサイクルフックという仕組みがあります。本記事では、初期化コストの高いワークロードの代表例としてML埋め込みモデル(multilingual-e5-small)を題材に、フック構成を変えた3パターンで初回推論レイテンシを比較しました。

ライフサイクルフックとは

デフォルトでは、スナップショットに初期化前の状態が含まれます。ライフサイクルフックを有効にすると、アプリケーション側で「準備完了」を宣言してからスナップショットが取得される流れに変わります。

本記事で扱うビルド時フックは2つです。

  • /ready フック — ビルド中にプラットフォームが POST /aws/lambda-microvms/runtime/v1/ready を送信。アプリケーションは初期化完了後に200を返し、その後スナップショット取得処理へ進む
  • /validate フック — スナップショットから復元後にプラットフォームが POST /aws/lambda-microvms/runtime/v1/validate を送信。アプリケーションは模擬処理を実行して200を返す。アクセスしたメモリページがprefetchのヒントとして記録される

アプリケーション構成

app.py

import os
import time
import psutil
from flask import Flask, request, jsonify
from sentence_transformers import SentenceTransformer

app = Flask(__name__)

MODEL_NAME = os.environ.get("MODEL_NAME", "intfloat/multilingual-e5-small")
MODEL_DIR = os.environ.get("MODEL_DIR", "/opt/model")

# グローバルスコープでモデルロード
_start = time.time()
model = SentenceTransformer(MODEL_DIR)
MODEL_LOAD_TIME = time.time() - _start

@app.route("/")
def health():
    mem = psutil.Process().memory_info().rss / 1024 / 1024
    return jsonify(
        model_name=MODEL_NAME,
        model_load_time_sec=round(MODEL_LOAD_TIME, 3),
        memory_usage_mb=round(mem, 1),
    )

@app.route("/embed", methods=["POST"])
def embed():
    data = request.get_json(force=True)
    text = data.get("text", "hello world")
    start = time.time()
    vec = model.encode([text])
    elapsed = time.time() - start
    return jsonify(
        elapsed_sec=round(elapsed, 4),
        shape=list(vec.shape),
    )

@app.route("/aws/lambda-microvms/runtime/v1/ready", methods=["GET", "POST"])
def ready():
    """ビルド時フック: モデルロード完了+ウォームアップ後に200"""
    model.encode(["warmup"])
    return jsonify(status="ready"), 200

@app.route("/aws/lambda-microvms/runtime/v1/validate", methods=["GET", "POST"])
def validate():
    """ビルド時フック: スナップショット復元検証+模擬推論でprefetchサンプリング"""
    model.encode(["validate prefetch sampling"])
    return jsonify(status="validated"), 200

ポイントは以下です。

  • グローバルスコープでモデルをロードしています。gunicornの --preload と組み合わせることで、ワーカーフォーク前にモデルロードが完了します
  • /ready エンドポイントではウォームアップ推論を1回実行してから200を返します。これにより、推論経路で必要になるPyTorch側の初期化やメモリ確保の多くを、スナップショット取得前に済ませることを狙っています
  • /validate エンドポイントでは模擬推論を実行します。この推論でアクセスされたメモリページがprefetchのヒントになります
  • フックのリクエストはhooks設定の port で指定したポート(本記事では9000)に届きます。フック自体は POST ですが、動作確認用に GET も許可しています。パスの詳細は注意事項を参照してください

Dockerfile

FROM public.ecr.aws/lambda/microvms:al2023-minimal

ARG MODEL_NAME=intfloat/multilingual-e5-small

RUN dnf install -y python3.11 python3.11-pip && dnf clean all && \
    ln -sf /usr/bin/python3.11 /usr/bin/python3 && \
    ln -sf /usr/bin/pip3.11 /usr/bin/pip3

COPY requirements.txt /opt/app/requirements.txt
RUN pip3 install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu && \
    pip3 install --no-cache-dir -r /opt/app/requirements.txt psutil

# モデルを事前ダウンロード
RUN python3 -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('${MODEL_NAME}').save('/opt/model')"

ENV MODEL_NAME=${MODEL_NAME}
ENV MODEL_DIR=/opt/model

COPY app.py /opt/app/app.py

WORKDIR /opt/app
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8080", "--bind", "0.0.0.0:9000", "--timeout", "120", "--workers", "1", "--preload"]
  • ベースイメージは public.ecr.aws/lambda/microvms:al2023-minimal です
  • torchはCPU版を指定しています。デフォルトではCUDAライブラリ(数GB)が含まれ、ビルド環境のディスク容量制限に引っかかるためです
  • モデルファイルはビルド時にダウンロードしてイメージに同梱しています。S3から都度ダウンロードする方式だと、ネットワーク遅延がフックの効果測定に混入するためです
  • gunicornは8080(アプリケーション用)と9000(フック用)の2ポートで同時にリッスンします。フックはビルド時にのみ呼ばれるため、本番トラフィックのポートと分離しています
  • --preload でワーカーフォーク前にモデルロードを完了させています。これがないとフックリクエスト到達時にロードが終わっておらずタイムアウトする可能性があります

requirements.txt

flask==3.1.1
gunicorn==23.0.0
sentence-transformers>=3.0.0

検証

環境

  • リージョン: ap-northeast-1(東京)
  • MicroVMサイズ: デフォルト(4 vCPU / 8 GBメモリ)
  • モデル: intfloat/multilingual-e5-small(~118Mパラメータ、384次元)
  • テスト文: "Lambda MicroVMs lifecycle hooks test"
  • 計測: curltime_total および /embed レスポンスの elapsed_sec で評価。model_load_time_sec はスナップショット保存時点の記録値であり、復元後のレイテンシではない
  • 合計初回レイテンシ: Run後のヘルスチェック(初回アクセス)の time_total と初回 /embedelapsed_sec を合算した値

パターン A: フックなし

フック設定を省略してイメージを作成しました。

aws lambda create-microvm-image \
  --image-name e5-small-no-hooks \
  --base-image-arn arn:aws:lambda:ap-northeast-1:aws:microvm-image:al2023-1 \
  --source-uri s3://microvm-webshell-123456789012/lifecycle-hooks/e5-small-no-hooks.zip \
  --role-arn arn:aws:iam::123456789012:role/MicroVMBuildRole
# ヘルスチェック(初回リクエスト = モデルロード発生)
$ curl -w "\nTIME: %{time_total}s\n" https://<endpoint>.lambda-microvm.ap-northeast-1.on.aws/
{"memory_usage_mb":726.0,"model_load_time_sec":18.438,"model_name":"intfloat/multilingual-e5-small"}
TIME: 18.171s

# 初回推論
$ curl -X POST -d '{"text":"Lambda MicroVMs lifecycle hooks test"}' \
  https://<endpoint>.lambda-microvm.ap-northeast-1.on.aws/embed
{"elapsed_sec":12.9989,"shape":[1,384]}

# 2回目
$ curl -X POST -d '{"text":"Lambda MicroVMs lifecycle hooks test"}' \
  https://<endpoint>.lambda-microvm.ap-northeast-1.on.aws/embed
{"elapsed_sec":0.0217,"shape":[1,384]}

初回リクエストでモデルロード(18.4秒)が発生し、続く初回推論で13秒かかりました。モデルは既にメモリ上にありますが、初回推論ではPyTorchの内部初期化(メモリアロケータやスレッドプール確保など)が発生するためです。合計の初回レイテンシは約31秒です。2回目のリクエストは0.02秒でした。スナップショットにモデルロード前の状態が保存されるため、Runのたびにこのコストが発生します。

パターン B: /ready フック

hooks設定で /ready を有効にしてイメージを作成しました。

aws lambda create-microvm-image \
  --image-name e5-small-ready \
  --base-image-arn arn:aws:lambda:ap-northeast-1:aws:microvm-image:al2023-1 \
  --source-uri s3://microvm-webshell-123456789012/lifecycle-hooks/e5-small-ready.zip \
  --role-arn arn:aws:iam::123456789012:role/MicroVMBuildRole \
  --hooks '{"port":9000,"microvmImageHooks":{"ready":"ENABLED","readyTimeoutInSeconds":300}}'

readyTimeoutInSeconds を300に設定しています(理由は注意事項を参照)。

# ヘルスチェック
$ curl -w "\nTIME: %{time_total}s\n" https://<endpoint>.lambda-microvm.ap-northeast-1.on.aws/
{"memory_usage_mb":762.2,"model_load_time_sec":4.674,"model_name":"intfloat/multilingual-e5-small"}
TIME: 2.087s

# 初回推論
$ curl -X POST -d '{"text":"Lambda MicroVMs lifecycle hooks test"}' \
  https://<endpoint>.lambda-microvm.ap-northeast-1.on.aws/embed
{"elapsed_sec":3.4128,"shape":[1,384]}

初回推論が3.4秒に短縮されました。モデルロード済み+ウォームアップ推論済みの状態がスナップショットに含まれるため、Run後はモデルロードを再実行せずに推論できます。残る5.5秒(ヘルスチェック2.1s + 推論3.4s)には、スナップショット復元後のページアクセス待ちやネットワーク往復などが含まれていると考えられます。

パターン C: /ready + /validate フック

hooks設定で /ready/validate の両方を有効にしました。

aws lambda create-microvm-image \
  --image-name e5-small-ready-validate \
  --base-image-arn arn:aws:lambda:ap-northeast-1:aws:microvm-image:al2023-1 \
  --source-uri s3://microvm-webshell-123456789012/lifecycle-hooks/e5-small-ready-validate.zip \
  --role-arn arn:aws:iam::123456789012:role/MicroVMBuildRole \
  --hooks '{"port":9000,"microvmImageHooks":{"ready":"ENABLED","readyTimeoutInSeconds":300,"validate":"ENABLED","validateTimeoutInSeconds":300}}'
# ヘルスチェック
$ curl -w "\nTIME: %{time_total}s\n" https://<endpoint>.lambda-microvm.ap-northeast-1.on.aws/
{"memory_usage_mb":761.5,"model_load_time_sec":1.929,"model_name":"intfloat/multilingual-e5-small"}
TIME: 2.082s

# 初回推論
$ curl -X POST -d '{"text":"Lambda MicroVMs lifecycle hooks test"}' \
  https://<endpoint>.lambda-microvm.ap-northeast-1.on.aws/embed
{"elapsed_sec":1.2885,"shape":[1,384]}

初回推論が1.3秒まで短縮されました。/validate 内の模擬推論でアクセスされたメモリページがprefetchヒントとして記録されます。今回の結果では初回推論レイテンシがさらに短縮されており、復元後のページアクセス待ちが軽減されたと考えられます。

結果比較

指標 A(フックなし) B(/ready) C(/ready+/validate)
model_load_time 18.4s 4.7s 1.9s
初回推論 13.0s 3.4s 1.3s
合計初回レイテンシ ~31s ~5.5s ~3.4s
2回目以降 0.02s 0.02s 0.015s
メモリ使用量 726MB 762MB 762MB

model_load_time は各イメージ作成時にアプリケーション内で記録された参考値であり、Run後の復元レイテンシを直接表すものではありません。体感の改善効果は「合計初回レイテンシ」行を参照してください。

フックありの構成では、ウォームアップ推論による追加のメモリ確保などの影響で、使用量がやや増加したと考えられます(726MB → 762MB)。

  • A → B(/ready追加): 82% 改善(31s → 5.5s)
  • A → C(/ready + /validate): 89% 改善(31s → 3.4s)
  • B → C(/validate追加): 合計初回レイテンシで 38% 改善(5.5s → 3.4s)

注意事項・Tips

フックのパスは /ready ではない

プラットフォームがフックとして送信するリクエストのパスは /aws/lambda-microvms/runtime/v1/ready です。/ready でルートを定義するとフックリクエストが404になり、タイムアウトでビルドが失敗します。

各フックのパスは以下です(lifecycle-model.md より)。

フック パス
ready POST /aws/lambda-microvms/runtime/v1/ready
validate POST /aws/lambda-microvms/runtime/v1/validate
run POST /aws/lambda-microvms/runtime/v1/run
resume POST /aws/lambda-microvms/runtime/v1/resume
suspend POST /aws/lambda-microvms/runtime/v1/suspend
terminate POST /aws/lambda-microvms/runtime/v1/terminate

readyTimeoutInSeconds の設定

デフォルトは60秒です。モデルロードとウォームアップ推論の合計時間がこれを超えるとビルドが失敗します。今回のe5-smallではモデルロード18秒+ウォームアップ数秒かかるため、余裕を持って300秒としました。

gunicorn の --preload を指定した理由

--preload によりワーカーフォーク前にアプリケーションモジュールをimportし、モデルロードを済ませた状態でプロセスを起動できます。--preload なしではワーカー起動後にモデルロードが始まるため、フックリクエスト到達時にロードが完了していないとタイムアウトする可能性があります。

まとめ

Lambda MicroVMsのライフサイクルフックを実装することで、MLモデルの初回アクセスから初回推論完了までの合計レイテンシを約31秒から約3.4秒に短縮できました。/ready フックだけでも82%の改善が得られ、/validate を追加すると89%改善に到達しました。

/ready によりモデルロードとウォームアップ推論をスナップショット取得前に済ませられ、/validate 追加分の短縮はprefetchヒントの効果と考えられます。今回の検証から、初期化コストの高いワークロードでは、ライフサイクルフックがRun後の初回待ち時間削減に有効であることを確認できました。Lambda MicroVMsでMLモデルのような初期化の重い処理を扱う場合は、初回レイテンシを抑える手段の一つとして検討してみてください。

参考リンク

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事