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_sizepreferred_batch_sizeの最大以上に。モデル自体がバッチ対応している必要あり(形状に-1が入るONNX/TFなど)。


調整の進め方(実務レシピ)

  1. ベースラインを取る
    • Dynamic Batching 無効dynamic_batchingブロック削除)で、perf_analyzerか本番トラフィックの影響が小さい時間帯に測る。
    • 収集: throughput, p50/p95/p99 latency, GPU使用率, CPU/メモリ, リクエストタイムアウト率。
  2. 小さく始める
    • max_queue_delay_microseconds: 500–2000(0.5–2ms)
    • preferred_batch_size: [ 2, 4, 8 ](まず小粒)
  3. メトリクスを見て段階的に拡張
    • p95 がSLO内→ preferred_batch_sizeを一段上げる(8,16)。
    • なお p95 悪化→ max_queue_delayを下げる or preferred_batch_sizeから大きい値を外す。
  4. リクエスト到着パターンを確認
    • バースト型なら max_queue_delay をやや増やす余地あり。
    • 均等到着なら小さめのmax_queue_delayで十分。
  5. 安定化の仕上げ
    • タイムアウト(クライアント側/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を検討。
  • タイムアウト地獄
    • 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を検討。

併用テクニック

  • 複数instance_groupでモデル複製(例: count: 2–4)→ キュー分散。
  • モデル間分離(重い/軽いでPodやGPUを分ける)→ 取り合い防止。
  • 入力整形の標準化(前処理でサイズ/形状を揃える)→ バッチ化成功率↑。

自動調整はできる?

Triton自体はメトリクスやリクエスト到着状況を見てmax_queue_delaypreferred_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”

投稿者 kojiro777

コメントを残す

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