
ModalでLLMをデプロイ:pip installから本番エンドポイントまで
LLMのセルフホスティングに関するほとんどのガイドは、最も困難な部分である「インフラストラクチャ」について軽く触れるだけです。CUDAドライバーとの格闘、Dockerイメージの管理、オートスケーリングの設定を行い、それでもなお深夜3時にアイドル状態のGPUに対して課金されてしまうことがあります。Modalは、そうした問題をすべて解消します。Pythonコードを書いて、デプロイすれば、URLが手に入ります。
このガイドでは、推論エンジンとしてvLLMを使用し、オープンソースLLMをModal上にデプロイする手順を解説します。最後まで読み終える頃には、誰も使用していないときはスケールトゥゼロ(稼働停止)し、H100 GPU上で動作する、稼働中のOpenAI互換APIエンドポイントが手に入っているはずです。
Modalとは何か(そしてなぜLLMに使うのか)?
Modalは、AIワークロード専用に構築されたサーバーレスコンピューティングプラットフォームです。AWS Lambdaのようなものですが、GPUサポート、秒単位の課金、Pythonネイティブの開発者体験を備えています。YAMLもDockerfileもKubernetesも不要です。Pythonスクリプト内でインフラ全体を定義し、単一のコマンドでデプロイできます。
LLMデプロイの定番となりつつある理由は以下の通りです:
- スケールトゥゼロ課金:エンドポイントがリクエストを処理していない間は、一切料金が発生しません
- 秒単位のGPU価格設定:H100が約$3.95/時間、A100 80GBが約$2.50/時間で、秒単位で課金されます
- サブ秒級のコールドスタート:特にメモリスナップショットを使用する場合、コンテナは高速に起動します
- 月額$30の無料クレジット:クレジットカード登録なしで実験するのに十分な額です
- DevOps不要:DockerビルドもTerraformもクラスター管理も必要ありません
LLMをローカルで実行しており、サーバー管理なしで適切なAPIを提供したい場合、Modalへの移行は最短ルートです。
Modal vs. RunPod vs. Lambda
| 機能 | Modal | RunPod | Lambda |
|---|---|---|---|
| 課金モデル | 秒単位、スケールトゥゼロ | 秒単位、最低課金額あり | 時間単位、常時稼働 |
| コールドスタート | 2〜4秒 | 6〜12秒(大規模コンテナ) | 該当なし(永続的) |
| GPU可用性 | H100, A100, L40S, T4 | A100, H100, A6000 | H100, A100 |
| インフラ構成 | Pure Python、設定ファイル不要 | Dockerベース、より細かな制御可能 | 完全なVMアクセス |
| 無料枠 | 月額$30クレジット | なし | なし |
| 適した用途 | バースト性のある開発ワークロード | 安定した推論トラフィック | 高利用率のトレーニング |
結論: Modalは、バースト性の高いワークロードや開発用途で優位性を発揮します。GPU利用率が一貫して40%を超える場合は、RunPodやLambda上の専用インスタンスの方が安価になります。それ以外の場合、プロトタイピング、断続的なAPI、デモなどでは、Modalのスケールトゥゼロモデルにより実質的なコスト削減が可能です。
前提条件
開始前に必要なものは3つだけです:
- ローカルにインストールされたPython 3.10以上
- Modalアカウント:modal.comで無料で登録
- Hugging Faceアカウント:モデルアクセス用(多くのモデルはゲート付きです)
これだけです。ローカルマシンにGPUは不要、CUDAツールキットも不要、Dockerも不要です。
ステップ1:Modalのインストールと認証
ターミナルを開き、Modal Pythonパッケージをインストールします:
pip install modal次に、セットアップコマンドを実行して、ローカル環境をModalアカウントにリンクします:
modal setupこれにより、認証用のブラウザウィンドウが開きます。確認すると、Modalがトークンをローカルに保存します。この作業は二度と行う必要はありません。
ステップ2:コンテナイメージの定義
ModalコンテナはPythonで定義されます。ベースイメージの指定、依存関係のインストール、環境変数の設定を、すべてコードとして記述します。app.pyというファイルを作成します:
import modal
# Define the container image with CUDA, Python, and vLLM
vllm_image = (
modal.Image.from_registry(
"nvidia/cuda:12.8.0-devel-ubuntu22.04", add_python="3.12"
)
.entrypoint([])
.pip_install(
"vllm==0.13.0",
"huggingface-hub==0.36.0",
)
)
app = modal.App("llm-endpoint", image=vllm_image)いくつか注目すべき点があります。Dockerfileは存在せず、modal.Imageチェーンがその役割を完全に担います。ベースイメージにはUbuntu 22.04とNVIDIA CUDA 12.8が含まれており、その上にvLLMとHugging Face Hubクライアントをインストールしています。
ステップ3:ボリュームを使用したモデルストレージの構成
LLMの重みは大容量です(7Bパラメータモデルはfp16で約14GB)。コンテナ起動ごとにダウンロードするのは避けたいところです。Modal Volumesを使用すると、コンテナに直接マウントされる永続ストレージを利用できます:
# Persistent volumes for caching model weights
hf_cache = modal.Volume.from_name("huggingface-cache", create_if_missing=True)
vllm_cache = modal.Volume.from_name("vllm-cache", create_if_missing=True)
MODEL_NAME = "Qwen/Qwen3-4B-Thinking-2507-FP8"
MODEL_REVISION = "953532f942706930ec4bb870569932ef63038fdf"ここでは、Qwen3-4B-Thinking (FP8)を使用しています。これは量子化された40億パラメータのモデルで、高速かつ高性能であり、単一のGPUに収まります。これをLlama 3.1 8B、Mistral 7B、またはvLLMがサポートする他の任意のHugging Faceモデルに置き換えることができます。
なぜFP8なのか? fp16と比較してメモリ使用量をほぼ半分に削減できるため、同じGPUでより大きなモデルを実行したり、より安価なGPUで小さなモデルを実行したりできます。量子化のトレードオフについて興味がある場合は、LLMをローカルで実行するためのガイドで精度フォーマットについて詳しく解説しています。
ステップ4:vLLMサーバー関数の作成
ここでModalの真価が発揮されます。Python関数にGPU要件、スケーリング設定、Webサーバーアノテーションをデコレートします。残りはModalがすべて処理します:
N_GPU = 1
MINUTES = 60
VLLM_PORT = 8000
@app.function(
gpu=f"H100:{N_GPU}",
scaledown_window=15 * MINUTES,
timeout=10 * MINUTES,
volumes={
"/root/.cache/huggingface": hf_cache,
"/root/.cache/vllm": vllm_cache,
},
)
@modal.concurrent(max_inputs=32)
@modal.web_server(port=VLLM_PORT, startup_timeout=10 * MINUTES)
def serve():
import subprocess
cmd = [
"vllm", "serve",
MODEL_NAME,
"--revision", MODEL_REVISION,
"--served-model-name", MODEL_NAME,
"--host", "0.0.0.0",
"--port", str(VLLM_PORT),
"--tensor-parallel-size", str(N_GPU),
"--enforce-eager", # Faster cold starts
]
subprocess.Popen(" ".join(cmd), shell=True)主要なデコレータを分解してみましょう:
gpu="H100:1":単一のH100 GPUをリクエストします。より安価な推論には"A100-80GB:1"、70B以上のモデルには"H100:2"に変更してくださいscaledown_window=15 * MINUTES:最後のリクエストから15分間コンテナをウォーム状態に保ち、その後スケールトゥゼロします@modal.concurrent(max_inputs=32):コンテナあたり最大32の同時リクエストを許可します(vLLMが内部でバッチ処理を行います)@modal.web_server(port=8000):vLLM HTTPサーバーをModal Webエンドポイントとして直接公開します--enforce-eager:コールドスタートを高速化するためにCUDAグラフのコンパイルをスキップします(トレードオフ:ピークスループットが若干低下)
scaledown_windowはコスト調整の主要なレバーです。開発用には5分、定期的なトラフィックが見込まれる本番APIには15〜30分に設定してください。
ステップ5:本番環境へのデプロイ
コマンドは1つだけです:
modal deploy app.pyModalはコンテナイメージをビルドし、レジストリにプッシュして、稼働中のURLを返します:
✓ Created objects.
├── 🔨 Created mount /app.py
├── 🔨 Created volume huggingface-cache
├── 🔨 Created volume vllm-cache
└── 🔨 Created web function serve => https://your-workspace--llm-endpoint-serve.modal.run初回のデプロイはモデルの重みをボリュームにダウンロードするため、数分かかります。以降のデプロイ(およびコールドスタート)は重みがキャッシュされているため、はるかに高速です。
開発中は代わりにmodal serve app.pyを使用してください。ファイルの変更に応じてホットリロードされ、一時URLが提供されます。
ステップ6:エンドポイントの呼び出し(OpenAI互換)
デプロイされたvLLMサーバーは、/v1/chat/completionsでOpenAI互換のAPIを公開します。標準のOpenAI Python SDKを使用して呼び出すことができます。base URLをModalのエンドポイントに向けるだけです:
from openai import OpenAI
client = OpenAI(
api_key="not-needed", # vLLM doesn't require auth by default
base_url="https://your-workspace--llm-endpoint-serve.modal.run/v1",
)
response = client.chat.completions.create(
model="Qwen/Qwen3-4B-Thinking-2507-FP8",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Explain what vLLM is in two sentences."},
],
temperature=0.7,
max_tokens=256,
)
print(response.choices[0].message.content)これはcurlでも動作します:
curl -X POST https://your-workspace--llm-endpoint-serve.modal.run/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen3-4B-Thinking-2507-FP8",
"messages": [{"role": "user", "content": "Hello!"}],
"max_tokens": 128
}'OpenAI互換APIをサポートするあらゆるツール(LangChain、LlamaIndex、独自アプリなど)で使用可能です。複数のLLMエンドポイント間でリクエストをルーティングしている場合は、LLMゲートウェイツールを使用すると、フェイルオーバーや負荷分散の管理に役立ちます。
コスト最適化のヒント
Modalの秒単位課金は時間単位の課金よりもすでに効率的ですが、さらに最適化する方法があります:
1. FP8量子化を使用する
FP8モデルは、fp16版と比較してVRAM使用量をほぼ半分に削減します。FP8のQwen3-8Bは単一のH100に収まりますが、fp16版はそのGPUの80GBの大部分を必要とします。VRAM使用量が少ないということは、小規模モデルに対してより安価なGPU(A100 40GB、L40S)を使用できることを意味します。
2. スケールダウンウィンドウを調整する
scaledown_windowパラメータは、最後のリクエスト後にコンテナがウォーム状態を維持する時間を制御します:
| シナリオ | 推奨ウィンドウ | 理由 |
|---|---|---|
| 開発/テスト | 5分 | コスト節約、コールドスタートは許容範囲 |
| 社内API(散発的) | 10〜15分 | コストとレイテンシのバランス |
| 本番(定期的なトラフィック) | 20〜30分 | コールドスタートを最小化 |
| 高トラフィック本番 | min_containers=1を使用 | 常に1つをウォーム状態に保持 |
3. 適切なGPUを選択する
H100をデフォルトにしないでください。小規模モデルには必要ありません:
| モデルサイズ | 推奨GPU | 概算コスト/時間 |
|---|---|---|
| 1-4B パラメータ | L4 または T4 | $0.59 - $0.80 |
| 7-8B パラメータ | A10 または L40S | $1.10 - $1.95 |
| 13-14B パラメータ | A100 40GB | $2.10 |
| 30-70B パラメータ | A100 80GB または H100 | $2.50 - $3.95 |
| 70B+ パラメータ | H100 x2 | $7.90 |
4. プロンプトキャッシングを有効にする
ワークロードに繰り返し使用されるシステムプロンプトや共有プレフィックスが含まれる場合、vLLMの自動プレフィックスキャッシングにより、レイテンシと計算コストを大幅に削減できます。vLLM serveコマンドに--enable-prefix-cachingを追加することで有効にできます。異なるプロバイダー間でのキャッシングの仕組みを深く探求したい場合は、LLMプロンプトキャッシングガイドをご覧ください。
5. コールドスタート最適化のために--enforce-eagerを使用する
デフォルトでは、vLLMは起動時にCUDAグラフをコンパイルしますが、これには追加で1〜3分かかります。--enforce-eagerフラグはこのコンパイルをスキップします。ピークスループットの約10〜15%を犠牲にする代わりに、コールドスタートを劇的に高速化できます。生スループットよりもレイテンシが重要なバースト性のあるワークロードでは、ほぼ常に正しい選択となります。
さらに進む:ファインチューニング済みモデル
ベースモデルのデプロイに慣れたら、次の自然なステップは独自のファインチューニング済みバージョンをデプロイすることです。ワークフローは同一で、MODEL_NAMEをHugging Faceリポジトリ、またはファインチューニング済みの重みを含むModalボリュームに向けるだけです。
Modalは、自社のGPU上でファインチューニングジョブを直接実行することもサポートしています。Modal上でLoRAアダプターをトレーニングし、ボリュームに保存して、マージ済みモデルをデプロイすることが、プラットフォームから離れることなく可能です。トレーニング側面については、LLMファインチューニングガイドで詳しく解説しています。
完全な app.py
コピー&ペーストですぐに使用できる完全なデプロイスクリプトはこちらです:
import modal
# --- Image Definition ---
vllm_image = (
modal.Image.from_registry(
"nvidia/cuda:12.8.0-devel-ubuntu22.04", add_python="3.12"
)
.entrypoint([])
.pip_install("vllm==0.13.0", "huggingface-hub==0.36.0")
)
# --- Volumes for Model Caching ---
hf_cache = modal.Volume.from_name("huggingface-cache", create_if_missing=True)
vllm_cache = modal.Volume.from_name("vllm-cache", create_if_missing=True)
# --- Model Config ---
MODEL_NAME = "Qwen/Qwen3-4B-Thinking-2507-FP8"
MODEL_REVISION = "953532f942706930ec4bb870569932ef63038fdf"
app = modal.App("llm-endpoint", image=vllm_image)
N_GPU = 1
MINUTES = 60
VLLM_PORT = 8000
@app.function(
gpu=f"H100:{N_GPU}",
scaledown_window=15 * MINUTES,
timeout=10 * MINUTES,
volumes={
"/root/.cache/huggingface": hf_cache,
"/root/.cache/vllm": vllm_cache,
},
)
@modal.concurrent(max_inputs=32)
@modal.web_server(port=VLLM_PORT, startup_timeout=10 * MINUTES)
def serve():
import subprocess
cmd = [
"vllm", "serve",
MODEL_NAME,
"--revision", MODEL_REVISION,
"--served-model-name", MODEL_NAME,
"--host", "0.0.0.0",
"--port", str(VLLM_PORT),
"--tensor-parallel-size", str(N_GPU),
"--enforce-eager",
]
subprocess.Popen(" ".join(cmd), shell=True)modal deploy app.pyでデプロイし、MODEL_NAMEを任意のHugging Faceモデルに置き換えれば、準備完了です。
よくある質問
ModalでLLMを実行するにはいくら費用がかかりますか?
GPUの種類とエンドポイントがウォーム状態を維持する時間によります。H100上のQwen3-4Bは、稼働中のみ約$3.95/時間です。スケールトゥゼロと15分のスケールダウンウィンドウを使用すれば、使用頻度の低いエンドポイントは月額$5〜15程度で済む可能性があります。月額$30の無料クレジットがあれば、多くの実験をカバーできます。
Modalはスケールトゥゼロしますか?
はい、それが主要なセールスポイントの一つです。scaledown_windowで設定した期間中にリクエストが届かない場合、コンテナはシャットダウンし、課金が停止します。次のリクエストでコールドスタートが発生します(モデルサイズや--enforce-eagerの使用状況により、通常2〜10秒かかります)。
ModalでLlama 3.1やMistralをデプロイできますか?
もちろん可能です。MODEL_NAME定数を、vLLMがサポートする任意のモデル(meta-llama/Llama-3.1-8B-Instruct、mistralai/Mistral-7B-Instruct-v0.3など、Hugging Face上の数百のモデル)に置き換えてください。70B以上のモデルの場合は、N_GPUを2に変更し、gpu="H100:2"を使用してください。
コールドスタートはRunPodと比較してどうですか?
Modalのコールドスタートは、コンテナ自体で通常2〜4秒、それにモデル読み込み時間が加算されます。Volumeにキャッシュされたモデル重みと--enforce-eagerを使用した場合、7-8Bモデルで合計10〜30秒程度です。RunPodのサーバーレスコールドスタートは、キャッシュ済みで200ms未満、大型コンテナで6〜12秒の範囲ですが、常時稼働モデルではコールドスタートが完全に回避されます。
ModalのvLLMエンドポイントは本当にOpenAI互換ですか?
はい。vLLMはOpenAIが使用するのと同じ/v1/chat/completions、/v1/completions、/v1/modelsエンドポイントを実装しています。公式のopenai Python SDKをModal URLに向けると、そのまま動作します。ストリーミング、関数呼び出し、JSONモードもすべて機能します。
ローカルマシンにGPUが必要ですか?
いいえ。ローカルマシンではModal CLIを実行するだけです。すべてのGPU処理はModalのクラウドインフラで行われます。必要であればChromebookからでもデプロイできます。
エンドポイントに認証を追加するにはどうすればよいですか?
Modal Webエンドポイントはデフォルトで公開されています。本番環境では、アプリケーションコードで簡単なAPIキーチェックを追加するか、Modalの組み込みWeb認証機能を使用してください。また、認証、レート制限、ルーティングを処理するLLMゲートウェイを使用してプロキシ層を設定することもできます。
modal serveとmodal deployの違いは何ですか?
modal serveは、コードを編集するとホットリロードされる一時エンドポイントを作成するため、開発に最適です。modal deployは、安定したURLを持つ永続的で本番-readyなエンドポイントを作成します。反復中はserveを使用し、リリース準備ができたらdeployを使用してください。
vLLM instead of SGLangを使用できますか?
はい。Modalのドキュメントには、vLLM alongside SGLangの例が含まれています。SGLangは、デコード重視のワークロードや小規模モデルにおいてオーバーヘッドが低い傾向があります。vLLMは、重いプリフィルを含む混合ワークロードで一般的に優れています。どちらもOpenAI互換のエンドポイントを生成します。
RailwayやRenderへのデプロイと比較してどうですか?
Railway、Render、Fly.ioなどのプラットフォームはWebアプリに適していますが、GPUインスタンスを提供していません。Modalは、秒単位課金とオートスケーリングを備えたGPUワークロード専用に構築されています。LLMを提供する必要がある場合、Modal(またはRunPod)が適切なツールであり、従来のPaaSプラットフォームでは対応できません。