TL;DR
迷ったら「モデルごとにBentoを分離Router/Gatewayで束ねる」。依存衝突を避け、ロールアウトも安全。将来は必要に応じて一体型(マルチランナー)アンサンブルへ拡張。


なぜ整理が必要か(よくあるカオス)

  • モデルごとにPython依存・CUDA版数が違い、環境地獄になる。
  • 1つのコンテナに全部詰めると、ビルドが重い/壊れやすい
  • APIがバラバラで、呼び出し側が結合してしまう。

BentoMLの強み

  • モデル/コード/依存を Bento(配布可能アーティファクト) に固定化。bentoml buildbentoml containerize でそのままコンテナ化。
  • 統一API(I/O仕様)で、KServeRay Serve、EKS/Knative、BentoCloudなどに持っていきやすい。

基本戦略(推奨順)

パターンA:モデルごとにBento分離 + Router/Gateway(推奨)

  • 依存衝突ゼロ。小さく始めてスケールしやすい。
  • ルーティングは リクエスト属性(モデル種別、言語、サイズ)や A/BCanary で制御。
  • デプロイ単位が小さく、ロールバックが速い。

パターン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)は薄いFastAPIBentoの別サービスで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で束ねる。
  • 可観測性と安全なロールアウトを前提に、マルチモデル運用をシンプルに始めよう。

投稿者 kojiro777

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です