🎓 レベル:標準 | 重要度:A(必須)
📎 前提:オンライン推論サービング | 関連:ハードウェアとスケーリング
要点(BLUF)
- 推論性能は2軸で測ります:レイテンシ(1件にかかる時間・小さいほど良い)とスループット(単位時間の処理数・大きいほど良い)。この2つはしばしばトレードオフします。
- 核心はバッチング:複数リクエストをまとめて1回の行列演算で処理すると、スループット(=単価)が上がる代わりに、まとめ待ちで個々のレイテンシが増えます。動的バッチングは「少し待って束ねる」ことでこの釣り合いを調整します。
- 監視・SLO で見るべきは平均でなくテールレイテンシ(p95/p99)。平均が速くても、p99 が遅ければ一部ユーザーの体験は壊れます。要件(許容遅延)の中で最大スループットを出すバッチサイズを選びます。
1. 2つの指標
| 指標 | 定義 | 改善= |
|---|---|---|
| レイテンシ | 1リクエストの応答時間 | 小さく |
| スループット | 単位時間あたり処理件数(QPS) | 大きく |
| テールレイテンシ | p95/p99(遅い方の裾) | 小さく(最重要) |
オンライン(デプロイパターン(バッチ・オンライン・ストリーミング))は低レイテンシ最優先、バッチは高スループット最優先。同じモデルでも、何を最適化するかで設計が変わります。
2. なぜバッチングがトレードオフを生むのか
GPU や BLAS は「たくさんをまとめて計算する」ほど1件あたりが安くなります(並列性・固定オーバーヘッドの償却)。だからリクエストを束ねればスループットは上がる。一方、束ねるには「ある程度たまるまで待つ」必要があり、最初に来たリクエストは待ち時間(遅延)が増えます。
flowchart LR R1["req到着"] --> Q["バッチキュー(少し待って束ねる)"] R2["req到着"] --> Q R3["req到着"] --> Q Q -->|"バッチで1回推論"| M["モデル"] M --> O["まとめて応答(単価減・個別遅延増)"]
3. 動く最小例:バッチサイズで遅延と単価がどう動くか
バッチサイズを変えて推論し、**1件あたりの処理時間(=単価の逆数)**がどう下がるかを実測します。
import time
import numpy as np
from sklearn.ensemble import RandomForestClassifier
rng = np.random.default_rng(0)
Xtr = rng.normal(0, 1, (2000, 20))
ytr = (Xtr[:, 0] + Xtr[:, 1] > 0).astype(int)
model = RandomForestClassifier(n_estimators=100, random_state=0).fit(Xtr, ytr)
pool = rng.normal(0, 1, (4096, 20))
print(f"{'batch':>6} | {'総時間ms':>8} | {'1件あたりms':>10} | {'相対スループット':>14}")
base_per = None
for bs in [1, 8, 32, 128, 512]:
# 同じ4096件を、バッチサイズ bs ずつ処理
t0 = time.perf_counter()
for i in range(0, len(pool), bs):
_ = model.predict(pool[i:i+bs])
total_ms = (time.perf_counter() - t0) * 1000
per_item = total_ms / len(pool)
if base_per is None:
base_per = per_item
print(f"{bs:>6} | {total_ms:>8.1f} | {per_item:>10.4f} | {base_per/per_item:>13.1f}x")
出力例(数値は環境依存・傾向が重要):
batch | 総時間ms | 1件あたりms | 相対スループット
1 | 12129.3 | 2.9613 | 1.0x
8 | 1563.9 | 0.3818 | 7.8x
32 | 392.3 | 0.0958 | 30.9x
128 | 133.9 | 0.0327 | 90.6x
512 | 46.0 | 0.0112 | 263.4x
出力の意味:バッチサイズを上げるほど1件あたりの時間が下がり(スループットが上がり)ます。ここでは RandomForest の predict 呼び出し1回あたりの固定オーバーヘッドが大きいため、batch=1(1件ずつ呼ぶ)が極端に遅く、batch=512 では約263倍の単価改善になりました。ただし限界収穫は逓減します:batch を 8 倍にした 1→8 では 7.8 倍ですが、その後は batch を 4 倍にするごとに約 3 倍ずつ(32→128→512)と、追加するバッチ幅に対する伸びは鈍ります。そして大きいバッチほど「束ねる待ち時間」で個々の遅延が増えるので、許容遅延(SLO)の範囲で十分な改善が得られるバッチサイズを選ぶのが実務です。単価のために遅延を無制限に犠牲にはできません。
4. 運用の勘所
- 遅延 SLO を先に決める:「p99 を◯ms 以内」を定め、その制約下で最大スループットになるバッチサイズ・並列度を探す。
- テール(p99)を見る:平均は速くても p99 が遅ければ一部ユーザーが苦しむ。監視・アラートは p95/p99 で(本番モデルの監視)。
- 動的バッチングのタイムアウトを設定する:「最大◯ms 待つ or ◯件たまる」で束ね、低トラフィック時に待ちすぎない。
- スループットが足りなければ水平スケール:1台のバッチングで限界なら台数を増やす(ハードウェアとスケーリング)。
なぜそうするのか
レイテンシとスループットを区別するのは、最適化の方向が問題によって正反対だからです。ユーザーが待つオンライン推論では遅延が王様、大量データを夜次で捌くバッチでは単価が王様。バッチングという同じ道具が、一方では味方(単価減)でもう一方では敵(遅延増)になります。だから「何を守るべきか(遅延 SLO)」を先に決め、その制約の中でもう一方(スループット)を最大化する、という順序が要ります。テールを見るのは、平均が嘘をつくからです——平均が速くても、p99 の遅さが実際のユーザー不満を生みます。
⚠️ よくある落とし穴
- 平均レイテンシだけ見る:p99 の悪化を見逃し、一部ユーザーの体験が壊れる。テールを監視する。
- バッチサイズを無闇に大きくする:単価改善は逓減し、遅延だけ悪化する。SLO 制約下の最小で十分なサイズを選ぶ。
- 遅延 SLO を決めずに最適化する:何を守るかが曖昧だと、スループットのために遅延を犠牲にしすぎる。
- 動的バッチングにタイムアウトを置かない:低トラフィック時に「束ねる相手」を待ち続けて遅延が悪化する。
対応 lab
- なし(概念ノート)。バッチサイズと単価の関係は本文 §3 に同梱。実サービングのバッチ遅延は オンライン推論サービング の lab(64件バッチで1件0.006ms)でも確認できます。
関連ノート
- オンライン推論サービング(バッチ予測の実装)
- デプロイパターン(バッチ・オンライン・ストリーミング)(遅延と単価の優先が配信形態で変わる)
- ハードウェアとスケーリング(スループット不足は水平スケール)
- 本番モデルの監視(p99 の監視)
- LLMの推論サービングと最適化基盤(LLMの連続バッチング)
- 第5章 推論の最適化 目次
- MLOps・AI基盤 全体目次