この記事の要点
• Pythonデコレータだけでサーバーレスコンテナ・GPU環境を起動
• 従量課金制でアイドル時のコスト0、秒単位課金で無駄を削減
• LLM推論・画像生成・バッチ処理・分散学習をシンプルなコードで実現
AI/MLワークロードの実行には高価なGPUインスタンスが必要ですが、常時起動させるとコストが膨らみます。Modalは、必要な時だけGPU/CPUを起動し、処理完了後は自動停止するサーバーレスプラットフォームとして、開発者のインフラ管理負担とコストを大幅に削減します。本記事では、Modalの特徴と実践的な利用方法を解説します。
Modalとは
Modalは、Pythonコードに数行のデコレータを追加するだけで、クラウド上のコンテナやGPUクラスタを自動起動するサーバーレスコンピュートプラットフォームです。開発者はDockerfile・Kubernetes設定・インフラ管理コードを一切書かずに、スケーラブルなAI/MLワークロードを実行できます。
主要な特徴
- ゼロ管理: Dockerイメージ・依存関係・GPU設定をModalが自動管理
- 秒単位課金: 実行時間のみ課金、アイドル時はコスト0
- 高速コールドスタート: コンテナ起動が数秒、GPU環境も10秒以内
- 並列スケーリング: 数千の並列タスクを自動でスケールアウト
- 統合ストレージ: Volume・Dict・Queueなど永続化・共有ストレージを標準装備
- スケジュール実行: cron式でバッチジョブを定期実行
- Web UI: リアルタイムログ・メトリクス・コスト分析をダッシュボードで確認
ポイント: Modalは「サーバーレス」と「コンテナ」の良いところ取りをしています。AWS Lambdaのような手軽さで、コンテナ並みの柔軟性(GPU・任意の依存関係・長時間実行)を実現しています。
なぜModalが必要か
従来のML基盤構築には、以下の課題がありました。
従来の課題
- インフラ構築の複雑さ: Kubernetes・Docker・GPU設定を習得し、構成ファイルを大量に書く必要がある
- コスト管理の難しさ: GPUインスタンスを常時起動すると月額数万〜数十万円のコストが発生
- スケーリングの手間: 並列実行数に応じて手動でインスタンスを増減する必要がある
- 環境の再現性: ローカルと本番で依存関係・Pythonバージョンが異なるとエラーが発生
Modalはこれらをすべてコードレベルで解決し、Pythonの関数定義と同じ感覚でクラウドリソースを扱えます。
アーキテクチャ
flowchart TB
subgraph Client["開発者環境"]
A1["Python Script<br/>@app.function()"]
end
subgraph Modal["Modal Cloud"]
B1["Scheduler"]
B2["Container Pool"]
B3["GPU Pool"]
B4["Volume Storage"]
B5["Log Aggregator"]
end
A1 -->|"modal run"| B1
B1 --> B2
B1 --> B3
B2 --> B4
B3 --> B4
B2 --> B5
B3 --> B5
- Scheduler: 関数呼び出しをキューに積み、適切なコンテナ/GPUに割り当て
- Container Pool: 各タスクを独立したコンテナで実行
- GPU Pool: NVIDIA A100・H100等のGPUをオンデマンドで割り当て
- Volume Storage: 複数タスク間でファイルを共有(モデル重み・データセット等)
- Log Aggregator: すべてのログをリアルタイムで収集・表示
基本的な使い方
インストール
pip install modal
modal setup # 初回のみ認証
最小限のサンプル
import modal
# Modalアプリを定義
app = modal.App("hello-modal")
# 関数をModalで実行
@app.function()
def hello(name: str) -> str:
return f"Hello, {name}!"
# ローカルから呼び出し
@app.local_entrypoint()
def main():
result = hello.remote("World")
print(result)
modal run hello.py
# 出力: Hello, World!
このコードは、hello関数をModal上のコンテナで実行します。ローカルのPython環境とは完全に独立した環境で実行されるため、依存関係の競合が発生しません。
実践メモ: @app.function() デコレータを付けるだけで、ローカル関数がクラウド関数に変わります。呼び出しは .remote() メソッドで行います。
GPU推論の例
LLMや画像生成モデルをGPU上で実行する例です。
LLM推論(Transformers + GPU)
import modal
# GPUイメージを定義
image = (
modal.Image.debian_slim()
.pip_install("transformers", "torch", "accelerate")
)
app = modal.App("llm-inference")
# A100 GPUで実行
@app.function(
image=image,
gpu="A100", # NVIDIA A100 GPU
timeout=600,
)
def generate_text(prompt: str) -> str:
from transformers import pipeline
# モデルをロード(初回のみダウンロード)
generator = pipeline(
"text-generation",
model="meta-llama/Llama-2-7b-chat-hf",
device=0,
)
result = generator(prompt, max_length=100)
return result[0]["generated_text"]
@app.local_entrypoint()
def main():
result = generate_text.remote("Explain quantum computing in simple terms:")
print(result)
modal run llm_inference.py
このコードは、A100 GPUを起動してLlama 2モデルで推論を実行し、処理完了後にGPUを自動停止します。
ポイント: gpu="A100" を指定するだけでGPU環境が用意されます。Dockerイメージの手動ビルドやCUDA設定は不要で、Modalが自動で最適化します。
並列実行とバッチ処理
複数のタスクを並列実行する例です。
画像バッチ処理
import modal
image = modal.Image.debian_slim().pip_install("Pillow", "requests")
app = modal.App("batch-image-processing")
# 複数タスクを並列実行
@app.function(image=image, cpu=2)
def process_image(url: str) -> dict:
from PIL import Image
import requests
from io import BytesIO
# 画像をダウンロード
response = requests.get(url)
img = Image.open(BytesIO(response.content))
# リサイズ
img_resized = img.resize((800, 600))
return {
"url": url,
"original_size": img.size,
"resized_size": img_resized.size,
}
@app.local_entrypoint()
def main():
image_urls = [
"https://example.com/image1.jpg",
"https://example.com/image2.jpg",
# ... 1000個のURL
]
# .map()で並列実行
results = list(process_image.map(image_urls))
for result in results:
print(result)
.map()メソッドは、引数のリストを並列に処理し、自動的にタスク数を調整します。
注意: 並列実行数はプランによって制限されます。無料プランでは同時実行数に上限があるため、大規模バッチ処理には有料プランを検討してください。
Web API化
Modal関数をHTTP APIとして公開できます。
FastAPIとの統合
import modal
from fastapi import FastAPI
from pydantic import BaseModel
image = modal.Image.debian_slim().pip_install("fastapi[standard]")
app = modal.App("api-server")
# FastAPIインスタンス
web_app = FastAPI()
class PredictionRequest(BaseModel):
text: str
class PredictionResponse(BaseModel):
sentiment: str
score: float
@web_app.post("/predict")
def predict(request: PredictionRequest) -> PredictionResponse:
# ダミー推論(実際はモデルを使用)
return PredictionResponse(
sentiment="positive",
score=0.95,
)
# ModalでAPIを公開
@app.function(image=image)
@modal.asgi_app()
def fastapi_app():
return web_app
modal deploy api.py
# 出力: https://your-app.modal.run
このコードは、FastAPI アプリケーションを Modal 上で公開します。スケーリング・ロードバランシング・HTTPS証明書がすべて自動設定されます。
実践メモ: modal deploy コマンドで常時起動のエンドポイントを作成できます。リクエストがない時は自動スリープし、リクエストが来ると数秒で起動します。
スケジュール実行
定期的にバッチジョブを実行する例です。
毎日データ収集
import modal
from datetime import datetime
app = modal.App("scheduled-scraper")
# 毎日午前3時に実行
@app.function(schedule=modal.Cron("0 3 * * *"))
def daily_scraper():
print(f"Running scraper at {datetime.now()}")
# データ収集処理
# ...
print("Scraper finished")
modal deploy scheduled_scraper.py
デプロイ後、Modal側で自動的にスケジュールされたタスクが実行されます。
Volumeによる永続化
モデル重みやデータセットをVolume(永続ストレージ)に保存し、複数実行間で再利用できます。
モデル重みのキャッシュ
import modal
# Volumeを定義
volume = modal.Volume.from_name("model-cache", create_if_missing=True)
image = modal.Image.debian_slim().pip_install("transformers", "torch")
app = modal.App("model-cache-demo")
@app.function(
image=image,
gpu="T4",
volumes={"/cache": volume}, # /cacheにVolumeをマウント
)
def load_model():
from transformers import AutoModelForCausalLM, AutoTokenizer
model_name = "gpt2"
cache_dir = "/cache/models"
# 初回のみダウンロード、2回目以降はキャッシュから読み込み
tokenizer = AutoTokenizer.from_pretrained(model_name, cache_dir=cache_dir)
model = AutoModelForCausalLM.from_pretrained(model_name, cache_dir=cache_dir)
# Volumeを永続化
volume.commit()
return "Model loaded successfully"
@app.local_entrypoint()
def main():
result = load_model.remote()
print(result)
初回実行時はモデルをダウンロードしてVolumeに保存し、2回目以降はVolumeから読み込むため、ダウンロード時間を短縮できます。
ポイント: Volumeは複数の関数・実行間で共有されるため、大きなモデル重みやデータセットを毎回ダウンロードする必要がありません。
ModalとAWS Lambdaの比較
Modalと似た用途で使われるAWS Lambdaとの違いを理解しておくと、適切なプラットフォームを選択できます。
| 項目 | Modal | AWS Lambda |
|---|---|---|
| 言語 | Python中心 | 多言語対応 |
| GPU対応 | ネイティブ対応 | 非対応(別サービス必要) |
| 実行時間制限 | 数時間〜無制限 | 15分まで |
| コールドスタート | 数秒〜10秒 | 数ミリ秒〜数秒 |
| 依存関係 | 任意(コンテナ) | サイズ制限あり |
| 料金 | 秒単位課金 | ミリ秒単位課金 |
軽量なAPI処理ならLambda、AI/MLワークロードや長時間バッチならModalが適しています。
ModalとKubernetesの比較
Kubernetesと比べると、Modalは以下の点で優れています。
| 項目 | Modal | Kubernetes |
|---|---|---|
| セットアップ | 数分 | 数日〜数週間 |
| 設定ファイル | Pythonコードのみ | YAML大量 |
| スケーリング | 自動(設定不要) | 手動設定が必要 |
| GPU管理 | 自動 | 複雑なCRD設定 |
| コスト最適化 | 自動停止・起動 | 手動でノード管理 |
ただし、Kubernetesは他のクラウドサービス(データベース・キューイング等)との統合が柔軟なため、既存のKubernetes環境があり、細かい制御が必要ならKubernetes、速さと手軽さを優先するならModalを選択します。
実践的なユースケース
1. LLM推論API
import modal
image = modal.Image.debian_slim().pip_install("transformers", "torch", "fastapi[standard]")
app = modal.App("llm-api")
# モデルをグローバルで保持
@app.cls(
image=image,
gpu="A100",
volumes={"/cache": modal.Volume.from_name("llm-models", create_if_missing=True)},
)
class LLMInference:
@modal.enter()
def load_model(self):
from transformers import pipeline
self.generator = pipeline(
"text-generation",
model="meta-llama/Llama-2-7b-chat-hf",
device=0,
cache_dir="/cache",
)
@modal.method()
def generate(self, prompt: str, max_length: int = 100) -> str:
result = self.generator(prompt, max_length=max_length)
return result[0]["generated_text"]
@app.local_entrypoint()
def main():
model = LLMInference()
result = model.generate.remote("Explain AI in simple terms:")
print(result)
@app.clsを使うと、モデルを1回だけロードし、複数の推論リクエストで再利用できます。
2. 動画処理バッチ
import modal
image = modal.Image.debian_slim().pip_install("opencv-python", "ffmpeg-python")
app = modal.App("video-processing")
@app.function(
image=image,
cpu=4,
memory=8192,
timeout=3600,
)
def process_video(video_url: str) -> dict:
import cv2
import requests
from io import BytesIO
# 動画をダウンロード
response = requests.get(video_url)
# OpenCVで処理
# ... (フレーム抽出・圧縮等)
return {
"video_url": video_url,
"frames": 120,
"duration": 60,
}
@app.local_entrypoint()
def main():
video_urls = [
"https://example.com/video1.mp4",
"https://example.com/video2.mp4",
# ... 100個
]
results = list(process_video.map(video_urls))
print(results)
3. データパイプライン
import modal
app = modal.App("data-pipeline")
@app.function(schedule=modal.Cron("0 * * * *")) # 毎時
def extract_data():
# データ抽出
return ["data1", "data2"]
@app.function()
def transform_data(data: list) -> list:
# データ変換
return [d.upper() for d in data]
@app.function()
def load_data(data: list):
# データベースに保存
print(f"Loaded {len(data)} records")
# パイプライン実行
@app.local_entrypoint()
def pipeline():
raw_data = extract_data.remote()
transformed = transform_data.remote(raw_data)
load_data.remote(transformed)
料金と制限
Modalはフリープランと有料プランを提供しています(具体的な価格は公式サイトで確認してください)。
フリープラン(目安)
- 月間実行時間: 限定的
- GPU: T4など低価格GPU
- 並列実行数: 数個まで
有料プラン(目安)
- 月間実行時間: 従量課金(秒単位)
- GPU: A100・H100など高性能GPU
- 並列実行数: 数千まで
- サポート: 優先サポート
実践メモ: Modalは使った分だけ課金されるため、開発時はフリープラン、本番では必要な時だけ有料プランに切り替える運用が可能です。
まとめ
Modalは、Pythonデコレータだけでサーバーレスコンテナ・GPU環境を起動できる革新的なプラットフォームです。従来のML基盤構築で必要だったKubernetes・Docker・GPU設定を完全に抽象化し、開発者はビジネスロジックに集中できます。
Modalを選ぶべきケース
- LLM推論・画像生成などGPUが必要なワークロード
- バッチ処理を並列化してコストを最適化したい
- インフラ管理を極限まで減らしたい
- 従量課金でアイドル時のコストを0にしたい
- Pythonで完結するワークフロー
ModalはPythonコードに数行のデコレータを追加するだけで、数千の並列タスクをGPU上で実行できます。AI/MLの民主化を加速するプラットフォームとして、今後さらに普及が進むと予想されます。