🎓 レベル:標準 | 重要度:A(必須)
要点(BLUF)
- オンライン推論サービングは「学習済みモデルを低遅延の API として公開する」こと。リクエストごとに「その場の入力」で予測を返します(デプロイパターン(バッチ・オンライン・ストリーミング)のオンライン)。
- 設計の鉄則は起動時に1度だけモデルをロードすること。リクエスト毎にロードすると遅延が跳ね、スループットが死にます。加えて入力スキーマ検証・ヘルスチェック・メタデータ公開が最小要件です。
- プロトコルは REST(人にも機械にも扱いやすい・デバッグ容易)か gRPC(低遅延・型安全・高スループット)。まず REST で始め、性能要件が厳しければ gRPC を検討します。変換器ごとロードしてスキューを防ぐ(学習推論スキューの防止)のが大前提です。
1. オンラインサービングの最小要件
API としてモデルを出すなら、最低限これらが要ります:
| 要件 | なぜ必要か |
|---|---|
| 起動時ロード | リクエスト毎ロードは致命的遅延。1度だけ読む |
| 入力検証 | 不正な特徴量数・型を弾く(422 で返す) |
| ヘルスチェック | ロードバランサ・k8s が生死を判定する |
| メタデータ公開 | どのモデル・版が動いているかを確認できる |
| 変換器同梱 | 前処理ごとロードしてスキュー防止 |
2. 図解:サービングの構成
flowchart LR C["クライアント"] -->|"POST predict(JSON)"| API["サービングAPI(FastAPI)"] API --> V["入力スキーマ検証"] V --> M["メモリ常駐モデル(起動時ロード)"] M --> R["予測+確率+遅延を応答"] LB["ロードバランサ"] -->|"GET health で生死判定"| API REG["モデルレジストリ Production"] -.->|"起動時にロード"| M
ポイントは、モデルがメモリに常駐し、レジストリの Production 版(モデルレジストリとモデルのライフサイクル)を起動時に読むこと。ヘルスチェックでロードバランサが生死を見ます。
3. REST と gRPC
| REST(JSON over HTTP) | gRPC(Protobuf over HTTP/2) | |
|---|---|---|
| 遅延 | 普通 | 低い |
| デバッグ | 容易(curl で叩ける) | やや手間(専用クライアント) |
| 型安全 | 緩い | 厳格(スキーマ定義) |
| 向き | 外部公開・汎用 | 内部の高スループット連携 |
迷ったら REST。マイクロサービス間の高頻度・低遅延通信が必要なら gRPC を足します。
4. 動く最小例:FastAPI による最小サービング
labs/04_serving/ に、学習済みパイプライン(前処理+分類器)を丸ごとロードして予測を返す最小サービングを置きました。中核は「起動時に1度ロード+Pydantic で入力検証」です:
# serve.py(抜粋)
MODEL = joblib.load(HERE / "model.joblib") # 起動時に1度だけ
META = json.loads((HERE / "model_meta.json").read_text(encoding="utf-8"))
N_FEATURES = META["n_features"]
class PredictRequest(BaseModel):
features: List[float]
@field_validator("features")
@classmethod
def check_len(cls, v):
if len(v) != N_FEATURES:
raise ValueError(f"features must have length {N_FEATURES}, got {len(v)}")
return v
@app.post("/predict")
def predict(req: PredictRequest):
X = np.array(req.features).reshape(1, -1)
proba = float(MODEL.predict_proba(X)[0, 1])
return {"prediction": int(proba >= 0.5), "probability": round(proba, 4)}
smoke_test.py が「学習 → uvicorn 起動 → 疎通 → 予測 → バリデーション」を自動実行します。実行結果:
[health] {'status': 'ok', 'model': 'demo_classifier', 'version': 1}
[metadata] n_features=4 acc=0.95
[predict] pred=1 prob=1.0 latency_ms=0.818
[batch] n=64 latency_ms=0.383 per_item_ms=0.006
[validation] 不正入力のstatus=422(422が正しい)
SMOKE TEST PASSED
出力の意味:API が起動し、ヘルスチェックが ok を返し、単一予測(1件 0.818ms)と64件バッチ予測が動きました。注目はバッチの1件あたり遅延が 0.006ms と単一の100分の1以下なこと——まとめて推論するほど単価が下がる(レイテンシとスループットのトレードオフ)。さらに特徴量数が違う不正入力は 422 で弾かれ、壊れた入力がモデルに届きません。これが「起動時ロード+入力検証」を備えた最小オンラインサービングの骨格です。
5. 運用の勘所
- モデルは起動時ロード、リクエストでは推論だけ:これを破ると遅延が桁で悪化する。
- 入力検証を必ず置く:不正入力を 422 で弾き、モデルに壊れたデータを渡さない。
- ヘルスチェックを2種持つ:起動可否(liveness)と受付可否(readiness)。モデルロード完了まで readiness を false に。
- タイムアウトと同時実行数を制御する:1リクエストの遅延上限と並列度を決め、過負荷で連鎖崩壊させない。
- レジストリのステージで参照する:起動時に
model:Productionを読み、昇格・ロールバックに追従する。
なぜそうするのか
「起動時ロード」を鉄則にするのは、オンラインサービングの価値が低遅延の即時応答にあるからです。モデルのロードは重い処理で、これをリクエスト毎に行えば応答が数百ミリ秒〜秒単位に膨らみ、オンラインである意味が消えます。一度メモリに常駐させ、リクエストでは推論計算だけを行う——この分離が低遅延を支えます。入力検証を置くのは、壊れた入力でモデルが予測不能な値を返し、下流を汚染するのを防ぐためです。
⚠️ よくある落とし穴
- リクエスト毎にモデルをロードする:遅延が桁で悪化。起動時に1度だけロードする。
- 入力検証を省く:特徴量数・型の異常がモデルに届き、予測不能な出力で下流を汚す。
- ヘルスチェックを liveness だけにする:モデルロード前にトラフィックが来て失敗する。readiness を分ける。
- 変換器を同梱しない:前処理が再現できずスキュー。Pipeline ごとロードする。
- 同期処理で重い推論を直列化する:1件の遅い推論が全体を詰まらせる。並行度・タイムアウトを設計する。
対応 lab
mlops-study/labs/04_serving/—train_model.py(学習・保存)/serve.py(FastAPI サービング)/smoke_test.py(起動〜疎通〜検証の統合テスト)。python smoke_test.pyで一括実行。
関連ノート
- デプロイパターン(バッチ・オンライン・ストリーミング)(オンラインを選ぶ判断)
- モデルレジストリとモデルのライフサイクル(Production を起動時にロード)
- 学習推論スキューの防止(変換器同梱の理由)
- レイテンシとスループットのトレードオフ(バッチングで単価を下げる)
- モデルのパッケージングとコンテナ化(API をコンテナに固める)
- 第4章 デプロイとサービング 目次
- MLOps・AI基盤 全体目次