TL;DR
迷ったら「モデルごとにBentoを分離 → Router/Gatewayで束ねる」。依存衝突を避け、ロールアウトも安全。将来は必要に応じて一体型(マルチランナー)やアンサンブルへ拡張。
なぜ整理が必要か(よくあるカオス)
- モデルごとにPython依存・CUDA版数が違い、環境地獄になる。
- 1つのコンテナに全部詰めると、ビルドが重い/壊れやすい。
- APIがバラバラで、呼び出し側が結合してしまう。
BentoMLの強み
- モデル/コード/依存を Bento(配布可能アーティファクト) に固定化。
bentoml build
→bentoml containerize
でそのままコンテナ化。 - 統一API(I/O仕様)で、KServe や Ray Serve、EKS/Knative、BentoCloudなどに持っていきやすい。
基本戦略(推奨順)
パターンA:モデルごとにBento分離 + Router/Gateway(推奨)
- 依存衝突ゼロ。小さく始めてスケールしやすい。
- ルーティングは リクエスト属性(モデル種別、言語、サイズ)や A/B、Canary で制御。
- デプロイ単位が小さく、ロールバックが速い。
パターンB:1サービスに複数Runner(依存が近いなら)
- 近い依存(例:同じPyTorch/CUDA)なら一体化でレイテンシのオーバーヘッド減。
- ただし、依存の将来衝突リスクを常に監視。
パターンC:アンサンブル/パイプライン(前処理→モデル→後処理)
- Ensembleで精度向上やフォールバック(軽量→重量)を実現。
- 失敗時のフォールバック経路を設計(タイムアウト時は軽量モデルへ等)。
最小スニペット(単一モデル)
# service.py
import bentoml
from bentoml.io import JSON
runner = bentoml.sklearn.get("clf_model:latest").to_runner()
svc = bentoml.Service("clf_service", runners=[runner])
@svc.api(input=JSON(), output=JSON())
def classify(input_data):
return runner.run(input_data)
依存の固定(例)
# bentofile.yaml
service: "service:svc"
python:
packages:
- scikit-learn==1.4.2
- bentoml==1.2.21
models:
- tag: clf_model:latest
ルーターで束ねる(パターンA)
- 各モデルは 独立Bento としてビルド・デプロイ。
- ルーター(Gateway)は薄いFastAPIやBentoの別サービスでHTTPリバースプロキシ/選択ロジックを持つ。
# router.py(Gateway側:軽量ロジックに徹する)
from fastapi import FastAPI, HTTPException
import httpx
app = FastAPI()
BACKENDS = {
"en": "http://svc-bert-en.default.svc.cluster.local/predict",
"ja": "http://svc-bert-ja.default.svc.cluster.local/predict",
}
@app.post("/predict")
async def route(req: dict):
lang = req.get("lang", "en")
url = BACKENDS.get(lang)
if not url:
raise HTTPException(400, f"unsupported lang: {lang}")
async with httpx.AsyncClient(timeout=2.0) as client:
r = await client.post(url, json=req)
return r.json()
ポイント
- ゲートウェイはステートレスに。リトライ/タイムアウトを明示。
- フィーチャートグルやCanary比率を 環境変数/設定ファイル で切替可能に。
1サービス複数Runner(パターンB)
# multi_service.py
import bentoml
from bentoml.io import JSON
clf = bentoml.sklearn.get("clf:latest").to_runner()
reg = bentoml.sklearn.get("reg:latest").to_runner()
svc = bentoml.Service("multi_svc", runners=[clf, reg])
@svc.api(input=JSON(), output=JSON())
def predict(req):
task = req.get("task")
x = req.get("input")
if task == "clf":
return {"task": task, "y": clf.run(x)}
elif task == "reg":
return {"task": task, "y": reg.run(x)}
else:
return {"error": "unknown task"}
bentofile.yaml(依存が近い前提)
service: "multi_service:svc"
python:
packages:
- scikit-learn==1.4.2
- bentoml==1.2.21
models:
- tag: clf:latest
- tag: reg:latest
ビルドとコンテナ化
# ビルド
bentoml build
# ローカルサーブ
bentoml serve .
# コンテナ化(OCIイメージ作成)
bentoml containerize multi_svc:latest
KServe / Ray Serve への展開の考え方
KServe(Knative/EKS等)
bentoml containerize
で作ったコンテナをそのままInferenceService
に載せる。- コンテナのポートとヘルスチェックを合わせる。
# inference_service.yaml(例)
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: clf-service
spec:
predictor:
containers:
- image: <your-registry>/clf_service:latest
name: user-container
ports:
- containerPort: 3000 # Bentoのデフォルトに合わせる
Ray Serve
- Router/GatewayをRay Serveで書く選択肢もアリ。
- 各Bentoは別デプロイ(EKS/Fargate等)に置き、Serveはルーティングとバックプレッシャに集中。
依存分離の実務Tips
- CUDA/torch/tensorrt は モデル単位でピン止め。混ぜない。
- Pythonはメジャーバージョン固定(3.10/3.11等)。
pip freeze
をBentoに入れず、必要最小限だけbentofile.yaml
に記載。- モデル重量(MB/GB) と ロード時間 をメトリクス化(p95/p99)。
バージョニングとロールアウト
- Bentoタグ:
<service>:<semver>-<gitsha>
で再現性。 - Canary(5%→25%→100%) と 自動ロールバック(エラー率/レイテンシ閾値)をGateway/Ingressで。
- Blue/Green:新旧を並走、切替はDNS/Ingressで瞬断ゼロ。
監視・SLO(最低限)
- メトリクス:
latency_ms{stage=pre/model/post}
,load_time_ms
,queue_len
,gpu_mem_bytes
,oom_count
- ログ:リクエストID/モデルタグ/入力サイズ/エラー種別。
- トレース:pre→model→post を span 分解。
- SLO例:
p95<150ms
(CPU)/p95<60ms
(GPU)、error_rate<0.5%
。
ありがちな落とし穴 → 対策早見表
落とし穴 | 何が起きる | 対策 |
---|---|---|
1イメージに全部詰める | 依存衝突/ビルド激重 | モデル分離 + Router |
同一GPUで重量モデル併用 | OOM/スローダウン | GPUクォータ/専有/ミグ(MIG) |
ロギング不足 | ロールバック判断不能 | エラー率/レイテンシをダッシュボード化 |
コールドスタート放置 | p95スパイク | ウォームアップ/minScale/プリローディング |
ルータ無保護 | 雪崩・再起動ループ | サーキットブレーカ/リトライx回 |
リポジトリ構成(例)
ml-system/
gateway/
app.py # ルーティング
requirements.txt
Dockerfile
services/
clf/
service.py
bentofile.yaml
Dockerfile # 任意(containerizeを使うなら省略可)
reg/
service.py
bentofile.yaml
deploy/
kserve/
clf.yaml
reg.yaml
helm/
values.yaml
FAQ
Q. まず何からやればいい?
A. 1) モデルごとにBento化 → 2) Gateway追加 → 3) 監視ダッシュボード → 4) Canary導入 の順。
Q. 動的にモデルを差し替えたい
A. ランナーのホットスワップは避け、新Bentoを横に出してルーティング切替が安全。
Q. 前処理/後処理はどこに?
A. できれば同じBentoにまとめる。共通処理はライブラリ化して各サービスへ配布。
仕上げ用スニペット集
ヘルスチェック(Bento)
@svc.api(input=JSON(), output=JSON())
def health(_: dict):
return {"ok": True}
ウォームアップ
@svc.on_startup
def warmup():
_ = runner.run([[0,0,0]])
Prometheusメトリクス(例)
import time
from prometheus_client import Counter, Histogram
REQ = Counter("req_total", "Total requests")
LAT = Histogram("latency_ms", "Latency in ms")
@svc.api(input=JSON(), output=JSON())
def predict(x):
REQ.inc()
t0 = time.time()
y = clf.run(x)
LAT.observe((time.time()-t0)*1000)
return {"y": y}
まとめ
- まずは分離、それから統合。
- 依存とリスクはBento単位で隔離、ビジネス要件はGatewayで束ねる。
- 可観測性と安全なロールアウトを前提に、マルチモデル運用をシンプルに始めよう。