TL;DR
- 小粒な推論リクエストはGPUを待機させがち → まとめて投げると行列演算が効く。
- Tritonの
dynamic_batching
は短時間キューイングして束ねる機能。レイテンシとのトレードオフ設計がカギ。 - まずは小さめの待機時間と少数の優先バッチサイズから検証し、p95レイテンシとスループットの両にらみで調整。
なぜ効く?(直感)
- GPUは大きな行列×ベクトルのほうが得意(スループットが上がる)。
- 1件ずつ処理=カーネル起動オーバーヘッドが相対的に大きい。
- 数件まとめる=カーネル起動回数↓、演算密度↑、メモリ転送効率↑。
CPUとGPUの仕組みの違い(イメージ図)
例として3つの処理があるとする。
処理A:10%程度を使用
処理B:60%程度を使用
処理C:30%程度を使用
A,B,Cをほぼ同時に順次CPU、GPUに処理させた際には以下のようなイメージで処理される。
※ 厳密にはCPUでも100%きっちりにはならないが、リソースをうまく埋められるという前提で示す
CPU(逐次処理)
A+B+C ⇒ [########################################] 100%
GPU(逐次処理)
A: [##........................................] 10%
B: [##############################............] 60%
C: [##############............................] 30%
⇒ GPUは本来並列計算が得意だが、小さい処理だと並列性を活かせず、各処理ごとに大半の演算器が遊んでしまう
GPU(Dynamic Batching)
A+B+C ⇒ [########################################] 100%
※ 複数リクエストをまとめることでGPUをフル稼働し、CPUのように効率よくリソースを埋められる
この図は「仕組みの違い」を直感的に示したもので、厳密な実測値ではありません。
最小構成(config.pbtxt 抜粋)
max_batch_size: 16
input [
{ name: "INPUT__0" data_type: TYPE_FP32 dims: [ 3, 224, 224 ] }
]
output [
{ name: "OUTPUT__0" data_type: TYPE_FP32 dims: [ 1000 ] }
]
instance_group [ { kind: KIND_GPU count: 1 } ]
dynamic_batching {
preferred_batch_size: [ 8, 16 ]
# 1ms 待つ。まずは小さく。
max_queue_delay_microseconds: 1000
}
注意:
max_batch_size
はpreferred_batch_size
の最大以上に。モデル自体がバッチ対応している必要あり(形状に-1
が入るONNX/TFなど)。
調整の進め方(実務レシピ)
- ベースラインを取る
- Dynamic Batching 無効(
dynamic_batching
ブロック削除)で、perf_analyzer
か本番トラフィックの影響が小さい時間帯に測る。 - 収集:
throughput
,p50/p95/p99 latency
, GPU使用率, CPU/メモリ, リクエストタイムアウト率。
- Dynamic Batching 無効(
- 小さく始める
max_queue_delay_microseconds: 500–2000
(0.5–2ms)preferred_batch_size: [ 2, 4, 8 ]
(まず小粒)
- メトリクスを見て段階的に拡張
- p95 がSLO内→
preferred_batch_size
を一段上げる(8,16
)。 - なお p95 悪化→
max_queue_delay
を下げる orpreferred_batch_size
から大きい値を外す。
- p95 がSLO内→
- リクエスト到着パターンを確認
- バースト型なら
max_queue_delay
をやや増やす余地あり。 - 均等到着なら小さめの
max_queue_delay
で十分。
- バースト型なら
- 安定化の仕上げ
- タイムアウト(クライアント側/Ingress側)をDynamic Batching導入後のp99に合わせて再設定。
- バックプレッシャー: レートリミット(ingress/nginx, service mesh)やキュー深さ監視を有効化。
ベンチ手順(perf_analyzer
例)
# 動的バッチ無効のベースライン
perf_analyzer -m my_model --concurrency-range 1:64 -i grpc --percentile=95
# 動的バッチ有効(上記configに変更後、リロード/再デプロイ)
perf_analyzer -m my_model --concurrency-range 1:64 -i grpc --percentile=95
観点
- 同一の同時実行レンジで**スループット↑**か?
- p95レイテンシがSLO許容内か?(例:200ms 以内)
- GPU利用率(nvidia-smi)と推論サーバの待機行列(Triton Metrics)に異常なし?
よくある落とし穴 → 対策
- 「バッチサイズを大きくすれば速い」は罠
- インタラクティブ用途(チャット/UI/音声)は待ち時間が体感に直撃。
- →
max_queue_delay_microseconds
は小さめ(1–3ms)から。SLOを必ず確認。
- モデルがバッチ非対応
- 入口は
-1
可変なのに内部がバッチ非対応でパディング地獄。 - → 前処理含めてバッチ軸で正しく積めるかをユニットテスト。
- 入口は
- 前後処理の単一スレッド化
- 推論は速いのに、前処理/後処理が直列で詰まる。
- → Python backendやDALI/NumPy並列化、
instance_group
増で吸収。
- シーケンス処理との混同
- ASR/TTSなどストリーム系は
sequence_batching
が適切な場合あり。 - → セッション継続/順序保証が必要なら
sequence_batching
を検討。
- ASR/TTSなどストリーム系は
- タイムアウト地獄
- Ingress/ALB/Clientが旧しきい値のまま。
- → p99+マージンで統一。タイムアウト→リトライ暴発→さらに渋滞…を防止。
監視すべきメトリクス(ダッシュボード指針)
- レイテンシ: p50/p95/p99(全体とモデル別)
- スループット: RPS / infer/sec
- キュー指標: 動的バッチのキュー滞留時間・形成率(Triton metrics)
- GPU: utilization, mem使用量, SM/DRAM busy
- エラー: TIMEOUT, TOO_BUSY, OOM, gRPC status codes
- 到着分布: リクエスト間隔ヒストグラム(バースト検知)
推奨の初期パラメータ(ユースケース別)
- 同期API(UI/REST/gRPC)
preferred_batch_size: [2,4,8]
max_queue_delay_microseconds: 500–2000
- バッチ非同期(ETL/集計/バッチ推論)
preferred_batch_size: [16,32,64]
max_queue_delay_microseconds: 5000–20000
- 音声/ストリーミング(低遅延重視)
- Dynamic Batchingは最小限。必要なら
sequence_batching
を検討。
- Dynamic Batchingは最小限。必要なら
併用テクニック
- 複数
instance_group
でモデル複製(例:count: 2–4
)→ キュー分散。 - モデル間分離(重い/軽いでPodやGPUを分ける)→ 取り合い防止。
- 入力整形の標準化(前処理でサイズ/形状を揃える)→ バッチ化成功率↑。
自動調整はできる?
Triton自体はメトリクスやリクエスト到着状況を見てmax_queue_delay
やpreferred_batch_size
を自動で変化させる機能は持っていません。基本は静的にconfig.pbtxt
で指定します。
ただし以下のような工夫で擬似的な自動調整は可能です。
1) Triton内での工夫(半自動)
- 優先度キュー+キューポリシー
リクエストに優先度を付け、レベルごとにmax_queue_delay_microseconds
を変えられます。SLO厳しめは短く、バッチ可は長く。dynamic_batching { preferred_batch_size: [4, 8, 16] priority_levels: 2 default_priority_level: 1 # 低優先度(=1)の待機を長めに default_queue_policy { default_timeout_microseconds: 2000 max_queue_delay_microseconds: 2000 } # 高優先度(=0)は短く priority_queue_policy { key: 0 value: { default_timeout_microseconds: 500 max_queue_delay_microseconds: 500 } } }
→ クライアント側で“急ぎ/非急ぎ”を付けるだけで、状況に応じて擬似的に自動切替できます。 - sequence_batching(ASR等のセッション系)
逐次到着でも順序を保ってまとめる。用途が合えば“待たせずにまとめる”が可能。
2) 外部制御ループ
- Tritonはメトリクスを豊富に出すので、制御ループを作るのが王道です。
- 監視指標(例)
p95/p99 latency
(SLO対比)nv_inference_queue_duration_us
(待機時間)nv_inference_request_success
(RPS)- GPU利用率/メモリ
- 制御ロジック(擬似コード)
if p95 < SLO*0.7 and GPU_util < 60%: // 余力がある→ まとめ方を強める bump(preferred_batch_size) or +Δ max_queue_delay elif p95 > SLO or timeout↑: // 遅くなってる→ レイテンシ優先 shrink(preferred_batch_size) or -Δ max_queue_delay
- 反映方法
- モデルリポジトリの入替+Model Repository API で reload(明示ロード/アンロード)。
- あるいは“設定違いのモデルを複数用意”しておいて、Gateway/ServiceMeshで重みを切替(Canaryの要領で%を変更)。ダウンタイム回避が容易。
- 監視指標(例)
3) クライアント側“マイクロバッチ”で自動化
- SDK側でサイズ閾値+時間閾値(例:最大32件 or 2ms)を持つ小さなキューを実装し、到着レートに合わせて自動で束ねる。
- サーバのDynamic Batchingと二段バッチにしておくと、低負荷時もレイテンシを荒らさず、高負荷時は自然にスループットが伸びる。
どれを選ぶ?
- まずは Triton の優先度キュー(コード変更最小)
- 次に 外部制御ループ(Prometheus + 小さなコントローラでAB切替)
- さらに クライアント側マイクロバッチを足すと“自動調整”感が強まります
失敗しないコツ
- “直接パラメータを書き換える”より“プロファイルを切り替える”
例:model-a-batch-soft
(遅延優先)とmodel-a-batch-aggro
(集約強め)を用意し、Gatewayでトラフィック比率を操作。ロールバックも一瞬。 - 優先度でSLO分離
緊急系は優先度0(短いdelay)、通常は優先度1(長め)に流し、同じGPUで共存させる。 - オートスケールと併用
KServe/Knativeなら待ち時間や同時実行をHPA/KPAの指標にしてレプリカ数は自動、バッチ度合いは前述の仕組みで自動に寄せる。
FAQ
Q. preferred_batch_size
はどう選ぶ?
A. 到着分布のモードに合わせる。平均到着レートが低いのに大きい値を入れると常にmax_queue_delay
で発車しやすい。
Q. gRPCとHTTPで違いは?
A. gRPCは多重化が効くため、同時実行/集約の相性が良い傾向。HTTP/1.1は接続数やNginx設定の影響を受けやすい。
Q. それでも遅い?
A. モデル最適化(TensorRT/半精度化)、I/O前後処理の並列化、モデル複製、より大きなGPU/より速いメモリ帯域を検討。
参考キーワード
- “triton dynamic_batching”
- “triton perf_analyzer”
- “triton sequence_batching”
- “triton config.pbtxt”