目次
はじめに
この記事は、株式会社asken (あすけん) Advent Calendar 2025の12/23の記事です。
こんにちは。AX推進部(テックリード・AIエンジニア)の山口です。
今回は、SageMakerでvLLMを動かす方法を説明したいと思います。
LLM を API として提供する際の課題
ChatGPT のような LLM を自社サービスに組み込みたい場合、以下のような課題があります:
- レイテンシ(応答時間): ユーザーがテキストを入力してから結果が返るまでの時間。長いとユーザー体験が悪化します
- スループット(処理能力): 同時に多くのリクエストを処理できるか
- コスト: GPU は高価なので、効率的に使いたい
- 運用負荷: サーバーの管理、スケーリング、監視など
- モデルの自由度: 目的に沿ったモデルを選択できるか、量子化モデルに対応できているかなど
なぜ SageMaker + vLLM なのか
AWSでのLLMのサービングには下記の方法が考えられます
| 観点 | SageMaker + vLLM | Bedrock(Custom Model Import) | ECS | EC2 |
|---|---|---|---|---|
| レイテンシ | ◎(vLLMで高並列に強い。AWSのLMIコンテナでvLLM対応) | ◎(マネージドで自動スケール) | ○(構成次第。コンテナ/インスタンス性能に依存) | ○〜△(最適化次第。全部自前でばらつきやすい) |
| 運用負荷 | ○(エンドポイント/スケール設定は必要だがマネージド) | ◎(ほぼゼロ運用) | △(クラスタ/ALB/スケール設計など自前) | ×(OS〜監視〜冗長化まで全部自前) |
| コスト / Scale-to-Zero | △(リアルタイムは常時課金。ServerlessはScale-to-Zero可能だがGPU非対応) (AWS ドキュメント) | △(オンデマンド。モデルコピー稼働分のみ、5分課金窓。無呼び出しでスケール0) | ○〜△(タスク数0は設定可。ただし「0→起動」をリクエスト起点でやるには工夫が要る) (AWS ドキュメント) | ○(止めれば0円。ただし自動停止/起動は自前実装) |
| 導入の容易さ | ○(AWS提供のLMIコンテナで比較的ラク) | ◎(S3に置いてインポート→Bedrock APIで即利用) | △(コンテナ化〜ECR〜タスク定義〜LBなど手数多め) | ×(環境構築から全部) |
| モデル自由度(制約) | ◎(ほぼ何でも。自分で制御できる) | △(対応アーキテクチャが限定:Mistral/Llama/Qwen/GPT-OSS/Mixtral等。さらに量子化モデル不可・Embedding不可など制約) | ◎(基本なんでも) | ◎(基本なんでも) |
この中でもSageMaker + vLLMを選んだ理由は下記です。
SageMaker + vLLM は、vLLMで高並列でも低レイテンシを狙いつつ、SageMakerのマネージド運用で現実的に回せるバランスが良いからです。
Bedrock Custom Import は運用は最軽量ですが対応モデル/構成の制約が強く、
ECS/EC2 は自由度は高い反面、運用・導入コストが重いため今回は外しました。
この記事で学べること
本記事を読むと、以下のことができるようになります:
- SageMaker 上で vLLM を動かす仕組みを理解する
LMI(Large Model Inference)の設定ファイル、serving.propertiesの設定方法を理解する- vLLM(
AsyncLLMEngine) を使ったカスタム推論ハンドラを実装する - パフォーマンスを最適化する方法を知る
対象読者
- AWS の基本的なサービス(S3、IAM)を使ったことがある方
- Python の基本的な文法を理解している方
- LLM を API として提供したいと考えている方
前提知識
LLM(Large Language Model)とは
大量のテキストデータで学習した AI モデルです。テキストを入力すると、続きのテキストを生成します。GPT(OpenAI)、Claude、Gemini などが有名です。
推論(Inference)とは
学習済みのモデルを使って、新しいデータに対する予測を行うことです。本記事では「ユーザーが入力したテキストに対して、モデルが応答を生成すること」を指します。
[入力] "今日の天気は?"
↓ 推論
[出力] "今日は晴れでしょう。気温は..."
GPU が必要な理由
LLM は数十億〜数千億のパラメータを持つ巨大なモデルです。これを高速に処理するには、並列計算が得意な GPU(Graphics Processing Unit)が必要です。
技術スタックの全体像
アーキテクチャ図
flowchart TB
subgraph AWS["AWS"]
subgraph Endpoint["SageMaker Endpoint"]
subgraph LMI["LMI コンテナ"]
subgraph DJL["DJL Serving"]
subgraph vLLM["vLLM (AsyncLLMEngine)"]
Model["gemma-3-4b-it<br/>モデル"]
end
end
end
end
S3[("S3 バケット<br/>(model.tar.gz)")]
end
Client["クライアント<br/>(API 呼び出し)"]
Client -->|"リクエスト"| Endpoint
Endpoint -->|"レスポンス"| Client
S3 -.->|"モデル読み込み"| vLLM
各コンポーネントの役割
| コンポーネント | 役割 | 例えるなら |
|---|---|---|
| SageMaker Real-time Endpoint | API のエンドポイントを提供。リクエストを受け付ける | レストランの受付 |
| LMI コンテナ | vLLM や必要なライブラリがインストール済みの環境 | キッチン設備一式 |
| DJL Serving | リクエストの受付・レスポンスの返却を担当 | ホールスタッフ |
| vLLM(AsyncLLMEngine) | 実際に LLM の推論を行うエンジン | シェフ |
| モデル (gemma-3-4b-it) | 学習済みの LLM 本体 | レシピ |
各コンポーネントの詳細解説
SageMaker Endpoint とは
Amazon SageMaker は、機械学習モデルの開発・デプロイを支援する AWS のサービスです。その中でも Real-time Endpoint は、API として即座に応答を返す推論エンドポイントを提供します。
メリット:
- インフラ管理が不要(サーバーの設定、OS のパッチ適用など)
- 自動スケーリング対応
- AWS の他サービス(Lambda、API Gateway など)との連携が容易
LMI(Large Model Inference)コンテナとは
AWS が提供する、大規模モデルの推論に特化したコンテナイメージです。
何が嬉しいのか
- vLLM、TensorRT-LLM などの推論エンジンがプリインストール済み
- 設定ファイル(
serving.properties)を書くだけで動作 - GPU の最適化が施されている
# 使用するコンテナイメージの例(LMI v18) 763104351884.dkr.ecr.ap-northeast-1.amazonaws.com/djl-inference:0.36.0-lmi18.0.0-cu128
DJL Serving とは
Deep Java Library (DJL) プロジェクトのモデルサービングフレームワークです。LMI コンテナの基盤となっています。
役割:
- HTTP リクエストの受付
- リクエストのデコード(JSON → Python オブジェクト)
- 推論エンジン(vLLM)への受け渡し
- レスポンスの返却
vLLM とは
vLLM は、UC Berkeley で開発された高速な LLM 推論エンジンです。
なぜ高速なのか:
- PagedAttention: GPU メモリを効率的に使用する技術
- Continuous Batching(連続バッチ処理): 複数のリクエストを効率的にまとめて処理
従来の方式(静的バッチ): リクエスト1: ████████░░░░ (長い) リクエスト2: ██░░░░░░░░░░ (短い) ← 待ち時間が発生 リクエスト3: ████░░░░░░░░ (中程度) ──────────────────────────▶ 時間 vLLM(連続バッチ): リクエスト1: ████████ リクエスト2: ██ → 終了後すぐに次のリクエストを処理 リクエスト3: ████ リクエスト4: ████████ ← 空いた枠に新しいリクエストを追加 ──────────────────────────▶ 時間
LLMEngine と AsyncLLMEngine
vLLM には2種類のエンジンがあります:
| エンジン | 特徴 | 用途 |
|---|---|---|
LLMEngine |
同期処理 | シンプルなバッチ処理 |
AsyncLLMEngine |
非同期処理 | リアルタイム API |
SageMaker LMI の async_mode=true と組み合わせる場合、AsyncLLMEngine を使うことで以下のメリットがあります:
- 並列リクエスト処理: 複数のリクエストを同時に処理
- 効率的なバッチ処理: vLLM の continuous batching と相性が良い
- レイテンシの安定化: 1つの長いリクエストが他のリクエストをブロックしない
AsyncEngineArgs の主要パラメータ
AsyncLLMEngine を初期化する際に使用する AsyncEngineArgs の主要パラメータを理解しておきましょう。これらは後の構築手順で model.py を作成する際に使用します。
engine_args = AsyncEngineArgs(
model=model_id,
dtype="bfloat16",
# GPU メモリ使用率(0.0〜1.0)
# 高いほど多くのリクエストを処理できるが、OOM のリスクが上がる
gpu_memory_utilization=0.9,
# 最大コンテキスト長
# 入力トークン数 + 出力トークン数 の上限
max_model_len=4096,
# 同時に処理できるシーケンス(リクエスト)数
# serving.properties の max_rolling_batch_size と合わせる
max_num_seqs=128,
# prefill と decode の並列化(次のセクションで詳しく解説)
enable_chunked_prefill=True,
# 1バッチあたりの最大トークン数
max_num_batched_tokens=4096,
)
async for による生成の仕組み
AsyncLLMEngine.generate() は async generator を返します:
async def inference(self, prompt: str): request_id = str(uuid.uuid4()) final_output = None # generate() は async generator # トークンが生成されるたびに yield される async for output in self.engine.generate( prompt, self.sampling_params, request_id ): # ストリーミング出力が必要な場合は # ここで中間結果を処理できる # print(output.outputs[0].text) # 今回は最終結果だけが必要なので、最後の出力を保持 final_output = output return final_output
generate() の動作イメージ: 時間 → yield 1: "今" yield 2: "今日" yield 3: "今日は" yield 4: "今日は晴" yield 5: "今日は晴れ" ... yield N: "今日は晴れでしょう。" (最終結果)
gemma-3-4b-it とは
Google が公開した Gemma ファミリーの マルチモーダルなLLM です。
- 3: バージョン 3
- 4b: 約 40 億パラメータ
- it: Instruction-tuned(指示に従うように調整済み)
約 40 億パラメータは「小型」に分類され、単一の GPU(24GB VRAM)で動作します。
※ gemma-3-270m-it, gemma-3-1b-itは、マルチモーダルではないので注意が必要です
パフォーマンス設計
構築手順に入る前に、パフォーマンスに関わる重要な設定について理解しておきましょう。これらの設定は serving.properties や model.py で使用しますが、意味を理解せずにコピー&ペーストすると、後でトラブルシューティングが困難になります。
Prefill と Decode の仕組み
LLM の推論には2つのフェーズがあります:
- Prefill(プリフィル): 入力プロンプト全体を処理して、最初のトークンを生成
- Decode(デコード): トークンを1つずつ生成
推論の流れ:
入力: "今日の天気は?"
↓
[Prefill] 入力全体を処理 → 最初のトークン "今" を生成
↓
[Decode] "今" → "日"
[Decode] "今日" → "は"
[Decode] "今日は" → "晴"
...
enable_chunked_prefill とは
enable_chunked_prefill=True を設定すると、Prefill フェーズを分割して、他のリクエストの Decode と並列実行できます。
従来の処理: リクエスト1: [=====Prefill=====][Decode][Decode][Decode] リクエスト2: 待機... [=====Prefill=====][Decode]... enable_chunked_prefill=True: リクエスト1: [==Prefill==][==Prefill==][Decode][Decode] リクエスト2: [==Prefill==][Decode][Decode] ※ Prefill を分割し、他のリクエストの Decode と並列実行
長いプロンプトを処理する際に特に効果があります。この設定は model.py の AsyncEngineArgs で使用します。
コールドスタート対策
コールドスタートとは
エンドポイントが停止状態(または新規起動)から、最初のリクエストを処理できるようになるまでの時間です。
コールドスタート:
リクエスト → [モデルロード: 数十分] → [推論: 100ms] → レスポンス
↑ ここが長い
ウォームスタート:
リクエスト → [推論: 100ms] → レスポンス
※ モデルは既にロード済み
LLM のモデルファイルは数 GB〜数十 GB あるため、コールドスタートには数十分かかることがあります。
Fast Model Loader の活用
serving.properties で option.enable_streaming_s3_download=true を設定すると、Fast Model Loader が有効になります:
従来の方式:
S3 → [全データダウンロード] → [GPU にロード]
└─────── 待機 ───────┘
Fast Model Loader:
S3 → [ダウンロードしながら GPU にロード]
└──── 並列処理 ────┘
これにより、コールドスタート時間を大幅に短縮できます。
model_loading_timeout の設定
大きなモデルをロードする場合、タイムアウトを適切に設定します:
# デフォルトは 240秒(4分) # 大きなモデルの場合は増やす option.model_loading_timeout=360 # 6分
タイムアウトが短すぎると、モデルのロード中にエラーになってしまいます。
構築手順
ここまでで、SageMaker + vLLM の仕組みとパフォーマンス設計のポイントを理解しました。いよいよ実際に構築していきましょう。
全体の流れ
構築は以下の 7 ステップで進めます:
| Step | 内容 | 成果物 |
|---|---|---|
| Step 1 | ディレクトリ構成を理解する | 必要なファイル一覧の把握 |
| Step 2 | serving.properties を作成する | LMI コンテナの設定ファイル |
| Step 3 | model.py を作成する | AsyncLLMEngine を使った推論ハンドラ |
| Step 4 | モデルをパッケージングする | model.tar.gz |
| Step 5 | S3 にアップロードする | S3 上のモデルファイル |
| Step 6 | SageMaker Endpoint を作成する | 稼働中の API エンドポイント |
| Step 7 | 推論を実行する | 動作確認 |
[Step 1-3: ファイル準備] → [Step 4-5: アップロード] → [Step 6-7: デプロイ・実行]
それでは、各ステップを詳しく見ていきましょう。
Step 1: ディレクトリ構成を理解する
SageMaker LMI でカスタム推論ハンドラを使用する場合、以下のファイルを用意します。
必要なファイル一覧
以下は google/gemma-3-4b-it を例にした実際のファイル構成です
my-model/ │ │ # ======================================== │ # モデル設定ファイル(必須) │ # ======================================== ├── config.json # モデルアーキテクチャ設定 │ # - レイヤー数、hidden size、attention heads │ # - 最大コンテキスト長、語彙サイズ │ # - model_type(AutoTokenizer/AutoProcessor が参照) │ │ # ======================================== │ # トークナイザー関連ファイル(必須) │ # ======================================== ├── tokenizer_config.json # トークナイザーの設定(★ chat_template を含む) │ # - tokenizer_class の指定 │ # - chat_template(Jinja2 テンプレート) │ # - 特殊トークンの設定 ├── tokenizer.json # トークナイザー本体(Fast Tokenizer 形式) │ # - 語彙、マージルール、正規化設定 ├── tokenizer.model # SentencePiece モデル │ # - Llama, Gemma 系で使用 ├── special_tokens_map.json # 特殊トークンのマッピング │ # - bos_token, eos_token, pad_token など ├── added_tokens.json # 追加トークン(fine-tuning で追加した場合) │ │ # ======================================== │ # チャットテンプレート(モデルによる) │ # ======================================== ├── chat_template.json # チャットテンプレート │ │ # ======================================== │ # プロセッサ設定(マルチモーダルモデルの場合) │ # ======================================== ├── preprocessor_config.json # 画像プロセッサの設定 │ # - 画像のリサイズ、正規化パラメータ │ # - Gemma 3 はマルチモーダル対応のため必要 ├── processor_config.json # プロセッサの設定 │ # - AutoProcessor が参照 │ # - processor_class の指定 │ │ # ======================================== │ # モデルの重み(必須) │ # ======================================== ├── model-00001-of-00002.safetensors # シャード分割された重み ├── model-00002-of-00002.safetensors # (gemma-3-4b-it は約 8.6GB) ├── model.safetensors.index.json # シャードのインデックス │ # - 各パラメータがどのファイルにあるか │ │ # ======================================== │ # 生成設定(推奨) │ # ======================================== ├── generation_config.json # デフォルトの生成パラメータ │ # - temperature, top_p, top_k │ # - max_new_tokens, do_sample │ │ # ======================================== │ # LMI 設定ファイル(必須) │ # ======================================== ├── serving.properties # ★ DJL Serving / vLLM の設定 └── model.py # ★ カスタム推論ハンドラ
各ファイルの役割
| ファイル | 必須 | 説明 |
|---|---|---|
config.json |
必須 | モデルアーキテクチャの定義。model_type は AutoTokenizer/AutoProcessor が参照 |
tokenizer_config.json |
必須 | トークナイザーの設定。chat_template(Jinja2 形式)を含む |
tokenizer.json |
推奨 | Fast Tokenizer の本体。高速なトークン化を実現 |
tokenizer.model |
モデルによる | SentencePiece モデル。Llama, Gemma 系で必要 |
special_tokens_map.json |
推奨 | 特殊トークン(BOS, EOS, PAD など)のマッピング |
added_tokens.json |
任意 | Fine-tuning で追加したトークン |
chat_template.json |
モデルによる | チャットテンプレート(別ファイルの場合) |
preprocessor_config.json |
マルチモーダル時 | 画像プロセッサの設定。画像入力を扱うモデルで必要 |
processor_config.json |
マルチモーダル時 | プロセッサの設定。AutoProcessor が参照 |
model.safetensors |
必須 | モデルの重み。SafeTensors 形式を推奨 |
model.safetensors.index.json |
シャード時 | 分割された重みファイルのインデックス |
generation_config.json |
推奨 | デフォルトの生成パラメータ |
serving.properties |
必須 | LMI コンテナの設定 |
model.py |
カスタム時 | カスタム推論ハンドラ |
chat_template.json について
chat_template.json は、チャット形式の入力(role/content の辞書リスト)を LLM が理解できるトークン列に変換するための テンプレート です。
# chat_template の使用例 messages = [ {"role": "user", "content": "こんにちは"}, ] # apply_chat_template() が chat_template.json 内の chat_template を使用 prompt = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True, # アシスタントの応答開始トークンを追加 )
テキスト専用 vs マルチモーダルモデル
| モデルタイプ | 必要なファイル | ロード方法 |
|---|---|---|
| テキスト専用 | tokenizer_config.json, tokenizer.json 等 |
AutoTokenizer.from_pretrained() |
| マルチモーダル | 上記 + preprocessor_config.json, processor_config.json |
AutoProcessor.from_pretrained() |
gemma-3-4b-it はテキストと画像の両方を処理できるマルチモーダルモデルのため、preprocessor_config.json と processor_config.json が含まれています。テキスト推論のみを行う場合でも、これらのファイルを含めておくと AutoProcessor でロードできます。
ファイルの取得方法
Hugging Face からモデルをダウンロードする場合、いくつかの方法があります。
方法1: huggingface_hub で全ファイルをダウンロード(推奨)
最も確実な方法は、huggingface_hub の snapshot_download を使用して必要なファイルを全てダウンロードすることです:
from huggingface_hub import snapshot_download model_id = "google/gemma-3-4b-it" save_dir = "./my-model" # 必要なファイルパターンを指定してダウンロード snapshot_download( repo_id=model_id, local_dir=save_dir, allow_patterns=[ # モデル設定ファイル "config.json", "generation_config.json", # トークナイザー関連(全て必要) "tokenizer.json", "tokenizer.model", "tokenizer_config.json", "special_tokens_map.json", "added_tokens.json", # チャットテンプレート(Gemma 3 の場合、別ファイルとしても存在) "chat_template.json", # マルチモーダル対応(Gemma 3 はマルチモーダルモデル) "preprocessor_config.json", "processor_config.json", # モデルの重み "*.safetensors", "model.safetensors.index.json", ], ) print(f"モデルを {save_dir} にダウンロードしました")
方法2: save_pretrained() を使用
transformers ライブラリの save_pretrained() メソッドを使う方法もあります。ただし、この方法では一部のファイル(chat_template.json など)が保存されない場合があるため、注意が必要です:
import torch from transformers import AutoTokenizer, AutoProcessor, AutoModelForCausalLM model_id = "google/gemma-3-4b-it" save_dir = "./my-model" # トークナイザーを保存 # tokenizer_config.json, tokenizer.json, tokenizer.model, # special_tokens_map.json, added_tokens.json が保存される tokenizer = AutoTokenizer.from_pretrained(model_id) tokenizer.save_pretrained(save_dir) # プロセッサを保存(マルチモーダルモデルの場合) # preprocessor_config.json, processor_config.json が保存される try: processor = AutoProcessor.from_pretrained(model_id) processor.save_pretrained(save_dir) except Exception as e: print(f"プロセッサの保存をスキップ: {e}") # モデルを保存 # config.json, *.safetensors, model.safetensors.index.json, # generation_config.json が保存される model = AutoModelForCausalLM.from_pretrained( model_id, dtype=torch.bfloat16, ) model.save_pretrained(save_dir) print(f"モデルを {save_dir} に保存しました")
方法3: Hugging Face CLI を使用
コマンドラインから直接ダウンロードすることもできます:
# huggingface_hub をインストール
pip install huggingface-hub
# モデルをダウンロード(全ファイル)
huggingface-cli download google/gemma-3-4b-it \\
--local-dir ./my-model \\
--include "*.json" "*.safetensors" "*.model"
ダウンロード後の確認
ダウンロードが正しく完了したか、以下のスクリプトで確認できます:
import os from pathlib import Path save_dir = Path("./my-model") # 必須ファイルのリスト required_files = [ "config.json", "tokenizer_config.json", "tokenizer.json", "generation_config.json", ] # 推奨ファイル(存在すれば OK) recommended_files = [ "tokenizer.model", "special_tokens_map.json", "added_tokens.json", "chat_template.json", "preprocessor_config.json", "processor_config.json", ] # モデル重みファイル weight_patterns = ["*.safetensors"] print("=== ファイル確認 ===\\n") # 必須ファイルのチェック print("【必須ファイル】") for f in required_files: exists = (save_dir / f).exists() status = "✓" if exists else "✗ (不足)" print(f" {status} {f}") # 推奨ファイルのチェック print("\\n【推奨ファイル】") for f in recommended_files: exists = (save_dir / f).exists() status = "✓" if exists else "- (なし)" print(f" {status} {f}") # モデル重みのチェック print("\\n【モデル重み】") safetensors_files = list(save_dir.glob("*.safetensors")) if safetensors_files: for f in safetensors_files: size_gb = f.stat().st_size / (1024**3) print(f" ✓ {f.name} ({size_gb:.2f} GB)") else: print(" ✗ .safetensors ファイルが見つかりません") # インデックスファイル index_file = save_dir / "model.safetensors.index.json" if index_file.exists(): print(f" ✓ model.safetensors.index.json")
Step 2: serving.properties を作成する
serving.properties は、DJL Serving(と vLLM)の動作を設定するファイルです。
# ======================================== # SageMaker LMI vLLM 設定ファイル # ======================================== # --- 基本設定 --- engine=Python # --- モデル設定 --- # モデルファイルの場所。SageMaker では /opt/ml/model に配置される option.model_id=/opt/ml/model # モデルの精度 option.dtype=bfloat16 # Hugging Face のカスタムコードを信頼するか # セキュリティ注意: この設定を true にすると、モデルリポジトリ内の # 任意の Python コードが実行される可能性があります。 # 信頼できるソース(公式リポジトリ等)のモデルのみで使用してください。 option.trust_remote_code=true # --- 非同期モード設定(重要!) --- # LMI v15 以降で推奨される設定 # true にすると複数リクエストを効率的に並列処理できる option.async_mode=true # ストリーミング出力を使用するか(今回は使用しない) option.enable_streaming=false # --- バッチ処理設定 --- # 同時に処理できる最大リクエスト数 option.max_rolling_batch_size=256 # --- GPU 設定 --- # 使用する GPU 数。max を指定すると利用可能な全 GPU を使用 option.tensor_parallel_degree=max # --- コールドスタート対策 --- # S3 からモデルをストリーミングダウンロード(高速化) option.enable_streaming_s3_download=true # モデル読み込みのタイムアウト(秒) option.model_loading_timeout=360 # --- カスタムハンドラ --- # 推論ロジックを記述した Python ファイル option.entryPoint=model.py
以下、各設定の詳細を解説します。
option.async_mode=true について
これは 最も重要な設定の一つ です。
async_mode=false(同期モード)の場合:
リクエスト1 → [処理中...] → 完了
リクエスト2 → [処理中...] → 完了
※ 1つずつ順番に処理
async_mode=true(非同期モード)の場合:
リクエスト1 → [処理開始]
リクエスト2 → [処理開始] ← 1 が終わる前に処理開始
リクエスト3 → [処理開始]
※ GPU の空き容量がある限り、並列で処理
option.dtype=bfloat16 について
モデルの数値精度を指定します。
| 精度 | メモリ使用量 | 特徴 |
|---|---|---|
| float32 (FP32) | 多い | 最も精度が高いが、メモリを多く消費 |
| float16 (FP16) | 少ない | メモリ効率が良いが、数値が不安定になることがある |
| bfloat16 (BF16) | 少ない | FP16 と同じメモリ効率で、FP32 に近い数値安定性 |
bfloat16 は「いいとこ取り」なので、最新の GPU(A10G、L4、H100 など)では bfloat16 を使うのが一般的です。
option.max_rolling_batch_size=256 について
一度に処理できるリクエストの最大数です。この値を大きくすると:
- メリット: スループット(単位時間あたりの処理量)が向上
- デメリット: GPU メモリを多く消費。大きすぎると Out of Memory (OOM) エラー
適切な値は、モデルサイズや GPU のメモリ容量によって異なります。
Step 3: model.py(カスタム推論ハンドラ)を作成する
model.py は、リクエストを受け取って推論を行い、結果を返す Python スクリプトです。
""" SageMaker LMI 用カスタム推論ハンドラ このファイルは以下の流れで処理を行います: 1. リクエストを受け取る 2. テキストをモデルが理解できる形式に変換(前処理) 3. vLLM で推論を実行 4. 結果を整形して返す(後処理) """ import logging import uuid from typing import Any # DJL Serving が提供するユーティリティ from djl_python import Input, Output from djl_python.async_utils import create_non_stream_output from djl_python.encode_decode import decode # Hugging Face のトークナイザー from transformers import AutoTokenizer # vLLM のコンポーネント from vllm import SamplingParams from vllm.engine.arg_utils import AsyncEngineArgs from vllm.engine.async_llm_engine import AsyncLLMEngine # ロガーの設定 logger = logging.getLogger(__name__) class InferenceHandler: """ 推論ハンドラクラス このクラスは以下を管理します: - vLLM エンジンの初期化 - リクエストの前処理 - 推論の実行 - レスポンスの後処理 """ def __init__(self): """コンストラクタ: 変数の初期化""" self.initialized = False # 初期化済みフラグ self.engine = None # vLLM エンジン self.tokenizer = None # トークナイザー self.sampling_params = None # 生成パラメータ def initialize(self, properties: dict): """ vLLM エンジンを初期化する この処理は最初のリクエスト時に1回だけ実行されます。 モデルの読み込みには数分かかることがあります。 Args: properties: serving.properties から読み込まれた設定 """ # 設定値の取得(デフォルト値付き) model_id = properties.get("model_id", "/opt/ml/model") dtype = properties.get("dtype", "bfloat16") gpu_memory_utilization = float( properties.get("gpu_memory_utilization", "0.9") ) logger.info(f"AsyncLLMEngine を初期化します: model={model_id}, dtype={dtype}") # ======================================== # AsyncEngineArgs: vLLM エンジンの設定 # ======================================== engine_args = AsyncEngineArgs( # --- 基本設定 --- model=model_id, # モデルのパス dtype=dtype, # 数値精度 trust_remote_code=True, # カスタムコードを信頼 # --- メモリ設定 --- gpu_memory_utilization=gpu_memory_utilization, # GPU メモリ使用率 # --- シーケンス設定 --- max_model_len=4096, # 最大コンテキスト長(入力+出力の合計トークン数) max_num_seqs=128, # 同時に処理できるシーケンス数 # --- パフォーマンス最適化 --- enable_chunked_prefill=True, # prefill と decode を並列化 max_num_batched_tokens=4096, # 1バッチあたりの最大トークン数 ) # ======================================== # AsyncLLMEngine の作成 # ======================================== # from_engine_args() で設定を渡してエンジンを作成 self.engine = AsyncLLMEngine.from_engine_args(engine_args) # ======================================== # トークナイザーの読み込み # ======================================== # モデルと同じトークナイザーを使用 self.tokenizer = AutoTokenizer.from_pretrained(model_id) # ======================================== # SamplingParams: テキスト生成のパラメータ # ======================================== self.sampling_params = SamplingParams( temperature=0.7, # 状況によって、適切な値を設定してください max_tokens=512, # 最大生成トークン数 ) self.initialized = True logger.info("初期化完了") def preprocess(self, data: dict) -> str: """ 入力データを LLM が理解できる形式に変換する 多くの LLM は「チャット形式」の入力を期待します。 トークナイザーの apply_chat_template() を使って変換します。 Args: data: {"text": "ユーザーの入力"} 形式の辞書 Returns: LLM への入力プロンプト(文字列) """ text = data.get("text", "") # チャット形式のメッセージを作成 messages = [ {"role": "user", "content": text}, ] # トークナイザーでプロンプト形式に変換 # add_generation_prompt=True で、モデルが応答を開始するためのプロンプトを追加 prompt = self.tokenizer.apply_chat_template( messages, tokenize=False, # トークン化せず文字列のまま返す add_generation_prompt=True, ) return prompt async def inference(self, prompt: str) -> Any: """ vLLM で推論を実行する AsyncLLMEngine.generate() は非同期ジェネレータを返します。 ストリーミング出力が不要な場合でも、async for でイテレートして 最終結果を取得します。 Args: prompt: 前処理済みのプロンプト Returns: vLLM の出力オブジェクト """ # 各リクエストを識別するための一意な ID request_id = str(uuid.uuid4()) final_output = None # generate() は async generator を返す # 生成が進むたびに中間結果が yield される async for output in self.engine.generate( prompt, # 入力プロンプト self.sampling_params, # 生成パラメータ request_id, # リクエスト ID ): # 最後の出力を保持(最終結果) final_output = output return final_output def postprocess(self, output: Any) -> dict: """ 推論結果を API レスポンス形式に変換する vLLM の出力から生成されたテキストを抽出します。 Args: output: vLLM の RequestOutput オブジェクト Returns: {"generated_text": "生成されたテキスト"} 形式の辞書 """ # outputs[0] は最初の(通常は唯一の)生成結果 generated_text = output.outputs[0].text.strip() return {"generated_text": generated_text} async def handle(self, data: list[dict]) -> list[dict]: """ バッチ推論のメインハンドラ 複数のリクエストを処理できますが、 ここでは順次処理しています。 Args: data: 入力データのリスト Returns: 結果のリスト """ results = [] for item in data: try: # 1. 前処理 prompt = self.preprocess(item) # 2. 推論 output = await self.inference(prompt) # 3. 後処理 result = self.postprocess(output) results.append(result) except Exception as e: logger.error(f"エラーが発生しました: {e}", exc_info=True) results.append({"error": str(e)}) return results # ======================================== # グローバルインスタンス # ======================================== # ハンドラは1つのインスタンスを使い回す(ウォームスタート最適化) _handler = InferenceHandler() async def handle(inputs: Input) -> Output: """ DJL Serving のエントリーポイント DJL Serving はこの関数を呼び出して推論を実行します。 async def で定義することで、非同期処理が可能になります。 Args: inputs: DJL の Input オブジェクト(リクエスト情報を含む) Returns: DJL の Output オブジェクト(レスポンス) """ global _handler # 初回リクエスト時のみ初期化を実行 if not _handler.initialized: _handler.initialize(inputs.get_properties()) # 空のリクエストは無視 if inputs.is_empty(): return None # ======================================== # リクエストのデコード # ======================================== # バッチ(複数リクエスト)を取得 batch = inputs.get_batches() raw_request = batch[0] # Content-Type を取得(通常は application/json) content_type = raw_request.get_property("Content-Type") # JSON をデコードして Python オブジェクトに変換 decoded_payload = decode(raw_request, content_type) # 単一リクエストの場合はリストに変換 if isinstance(decoded_payload, dict): data = [decoded_payload] else: data = decoded_payload # ======================================== # 推論の実行 # ======================================== results = await _handler.handle(data) # 単一結果の場合はリストから取り出す if len(results) == 1: results = results[0] # ======================================== # レスポンスの作成 # ======================================== # create_non_stream_output() で DJL の Output オブジェクトに変換 return create_non_stream_output(results)
Step 4: モデルをパッケージングする
SageMaker にデプロイするには、モデルファイルと設定ファイルを model.tar.gz にまとめる必要があります。
パラメーターが数千億ある大きなモデルの場合は、非圧縮でデプロイする方法も検討してください。
詳細は Deploying Your Endpoint (DJL) を参照してください。
パッケージング前のチェックリスト
パッケージングする前に、必要なファイルが揃っているか確認しましょう:
# ディレクトリ構成を確認 ls -la my-model/ # 期待されるファイル一覧: # ├── config.json # 必須: モデル設定 # ├── tokenizer_config.json # 必須: トークナイザー設定 # ├── tokenizer.json # 推奨: Fast Tokenizer # ├── tokenizer.model # Gemma/Llama系: SentencePiece # ├── special_tokens_map.json # 推奨: 特殊トークン # ├── generation_config.json # 推奨: 生成パラメータ # ├── model.safetensors # 必須: モデルの重み # │ (または model-00001-of-XXXXX.safetensors + index.json) # ├── serving.properties # 必須: LMI 設定 # └── model.py # カスタムハンドラ使用時
ファイル検証スクリプト
パッケージング前に、AutoTokenizer でロードできるか確認することをお勧めします:
from transformers import AutoTokenizer, AutoConfig model_dir = "./my-model" # config.json の検証 config = AutoConfig.from_pretrained(model_dir) print(f"model_type: {config.model_type}") # model_type が表示されればOK # トークナイザーの検証 tokenizer = AutoTokenizer.from_pretrained(model_dir) print(f"tokenizer_class: {tokenizer.__class__.__name__}") # テストエンコード test_text = "こんにちは" tokens = tokenizer.encode(test_text) print(f"tokens: {tokens}") print(f"decoded: {tokenizer.decode(tokens)}")
パッケージング
# ディレクトリに移動 cd my-model # tar.gz にパッケージング # 重要: ファイルはアーカイブのルートに配置する必要がある tar -czvf ../model.tar.gz . # 内容を確認(ルートにファイルがあることを確認) tar -tzvf ../model.tar.gz # 期待される出力例: # ./config.json # ./tokenizer_config.json # ./tokenizer.json # ./tokenizer.model # ./special_tokens_map.json # ./generation_config.json # ./model.safetensors # ./serving.properties # ./model.py
tar.gz とは: 複数のファイルを1つにまとめ(tar)、圧縮(gz)したアーカイブ形式です。
Step 5: S3 にアップロードする
パッケージングしたモデルを S3 にアップロードします。
# S3 バケットにアップロード aws s3 cp model.tar.gz s3://your-bucket-name/models/model.tar.gz # アップロードを確認 aws s3 ls s3://your-bucket-name/models/
Step 6: SageMaker Endpoint を作成する
SageMaker Endpoint の作成は、以下の 3 つのリソースを順番に作成します:
1. Model → 「どのコンテナで、どのモデルを使うか」を定義 2. EndpointConfig → 「どのインスタンスタイプで、何台動かすか」を定義 3. Endpoint → 実際にエンドポイントを起動
インスタンスタイプの選択
まず、使用するインスタンスタイプを決めましょう。モデルサイズに応じて適切なインスタンスを選択します:
| インスタンスタイプ | GPU | VRAM | 用途 |
|---|---|---|---|
| ml.g5.xlarge | NVIDIA A10G | 24GB | 小〜中規模モデル(〜13B) |
| ml.g6.xlarge | NVIDIA L4 | 24GB | 小〜中規模モデル(〜13B)、コスト効率が良い |
| ml.g5.12xlarge | NVIDIA A10G x4 | 96GB | 大規模モデル(70B〜) |
| ml.p4d.24xlarge | NVIDIA A100 x8 | 320GB | 超大規模モデル |
gemma-3-4b-it(約 4B パラメータ)は、ml.g5.xlarge や ml.g6.xlarge で十分動作します。
Service Quotas(クォータ)の事前確認
重要: ml.g6.xlarge などの GPU インスタンスは、デフォルトでは利用制限(クォータ)が 0 に設定されている場合があります。初めて使用する場合は、事前にクォータの引き上げ申請が必要です。
クォータ申請の手順:
- AWS マネジメントコンソールで「Service Quotas」を開く
- サービス一覧から「Amazon SageMaker」を選択
- 以下のクォータを検索し、必要な数を申請:
ml.g6.xlarge for endpoint usage- エンドポイント用ml.g6.xlarge for processing job usage- 処理ジョブ用(必要に応じて)
- 「クォータの引き上げをリクエスト」をクリック
- 必要なインスタンス数を入力して申請
申請例: - クォータ名: ml.g6.xlarge for endpoint usage - 現在の値: 0 - リクエスト値: 1(または必要な数)
申請時の注意点:
- 申請から承認まで数時間〜数日かかる場合があります
- 本番環境では余裕を持った数を申請しておくことを推奨します
- リージョンごとに申請が必要です(例: ap-northeast-1 と us-east-1 は別々に申請)
エンドポイントの作成
クォータの準備ができたら、Python(boto3)を使って SageMaker Endpoint を作成します。
import boto3 # SageMaker クライアントを作成 sagemaker_client = boto3.client('sagemaker') # ======================================== # 1. SageMaker Model を作成 # ======================================== # 「どのコンテナで、どのモデルを使うか」を定義 sagemaker_client.create_model( ModelName='my-llm-model', # 任意の名前 PrimaryContainer={ # LMI v18 コンテナイメージ 'Image': '763104351884.dkr.ecr.ap-northeast-1.amazonaws.com/djl-inference:0.36.0-lmi18.0.0-cu128', # S3 上のモデルファイル 'ModelDataUrl': 's3://your-bucket-name/models/model.tar.gz', }, # SageMaker がモデルにアクセスするための IAM ロール ExecutionRoleArn='arn:aws:iam::123456789012:role/SageMakerExecutionRole' ) print("Model 作成完了") # ======================================== # 2. Endpoint Configuration を作成 # ======================================== # 「どのインスタンスタイプで、何台動かすか」を定義 sagemaker_client.create_endpoint_config( EndpointConfigName='my-llm-config', # 任意の名前 ProductionVariants=[{ 'VariantName': 'AllTraffic', # バリアント名 'ModelName': 'my-llm-model', # 上で作成した Model 名 'InstanceType': 'ml.g6.xlarge', # GPU インスタンスタイプ 'InitialInstanceCount': 1, # 初期インスタンス数 }] ) print("Endpoint Configuration 作成完了") # ======================================== # 3. Endpoint を作成 # ======================================== # 実際にエンドポイントを起動(数分かかります) sagemaker_client.create_endpoint( EndpointName='my-llm-endpoint', # 任意の名前 EndpointConfigName='my-llm-config', # 上で作成した Config 名 ) print("Endpoint 作成開始(数分かかります)") # ======================================== # 4. Endpoint の状態を確認 # ======================================== import time while True: response = sagemaker_client.describe_endpoint( EndpointName='my-llm-endpoint' ) status = response['EndpointStatus'] print(f"Endpoint Status: {status}") if status == 'InService': print("Endpoint が利用可能になりました!") break elif status == 'Failed': print("Endpoint の作成に失敗しました") break time.sleep(30) # 30秒待機
クォータ不足時のエラー例
クォータが不足している場合、エンドポイント作成時に以下のようなエラーが発生します:
ResourceLimitExceeded: An error occurred (ResourceLimitExceeded) when calling the CreateEndpoint operation: The account-level service limit 'ml.g6.xlarge for endpoint usage' is 0 Instances, with current utilization of 0 Instances and a request delta of 1 Instances. Please use AWS Service Quotas to request an increase for this quota.
このエラーが出た場合は、前述の「Service Quotas(クォータ)の事前確認」の手順でクォータを申請してください。
Step 7: 推論を実行する
エンドポイントが作成できたら、推論を実行してみましょう。
import boto3 import json # SageMaker Runtime クライアント(推論用) runtime_client = boto3.client('sagemaker-runtime') # ======================================== # 推論の実行 # ======================================== response = runtime_client.invoke_endpoint( EndpointName='my-llm-endpoint', ContentType='application/json', Body=json.dumps({ 'text': 'こんにちは!今日の天気はどうですか?' }) ) # レスポンスを読み取り result = json.loads(response['Body'].read().decode('utf-8')) print("入力:", 'こんにちは!今日の天気はどうですか?') print("出力:", result['generated_text'])
まとめ
SageMaker + vLLM で LLM 推論基盤を構築するポイントをまとめます:
構築のステップ
- serving.properties を作成: LMI コンテナの設定
- model.py を作成: AsyncLLMEngine を使った推論ハンドラ
- model.tar.gz にパッケージング: モデルファイルと設定を1つにまとめる
- S3 にアップロード: SageMaker がアクセスできる場所に配置
- SageMaker Endpoint を作成: Model → Config → Endpoint の順に作成
重要な設定
| 設定 | 推奨値 | 理由 |
|---|---|---|
option.async_mode |
true |
LMI v15 以降推奨。並列処理を効率化 |
option.dtype |
bfloat16 |
メモリ効率と精度のバランス |
option.enable_streaming_s3_download |
true |
コールドスタートを高速化 |
enable_chunked_prefill |
True |
prefill/decode の並列化 |
AsyncLLMEngine のポイント
- 非同期処理で複数リクエストを効率的に処理
async forで生成結果をイテレートSamplingParamsで生成パラメータを制御
参考リンク
AWS 公式ドキュメント
DJL / LMI ドキュメント
- DJL LMI Configuration Guide
- Model Artifacts for LMI - 必要なファイル一覧
- vLLM User Guide (DJL)
- Available LMI Container Images
- DJL Serving Operation Modes
vLLM ドキュメント
Hugging Face ドキュメント
- AutoTokenizer - トークナイザーの自動ロード
- Tokenizers - Fast Tokenizer の解説
- save_pretrained - モデル保存方法
採用について
askenではエンジニアを絶賛募集中です。
まずはカジュアルにお話しできればと思いますので、ぜひお気軽にご連絡ください!
https://hrmos.co/pages/asken/jobs