ステータス:下書き(2026-10-16)、一部実測済み。 バッチごとの遅延は、自宅ラボの8GBクラスのノートPC向けGPU 1枚で、単発のPodとして測定しました(他のGPU作業なし、ウォームアップ後の繰り返しの中央値)。セッションのシミュレーションは、そのテーブルを用いたノートPCのCPUによる計算です(
code/06_batching_sim.py、1秒前後)。実際のワーカーの同時セッション値は、TT Scoreパフォーマンスドキュメントの負荷試験(サーバーGPU、他の作業とカードを共有していた時点)から引用しました。 シリーズ「ビジョンモデルを読む 3」第6回。遅延とスループットの区別については、 第1回にあります。
目次
- 1. 直感:バスとタクシー
- 2. サーバー側の研究:遅延目標内でのバッチ化
- 3. 実際に測定してみる:空モデルはバッチ化すると高速化するのか
- 4. 複数のセッションが1つのGPUを使用する場合
- 5. GPUを共有する方法
- 6. 制限事項
- 7. 我々のプロジェクトとのつながり
- 参考文献
1. 直感:バスとタクシー
GPUは数千個の計算ユニットを持つバスです。乗客(入力)を1人だけ乗せて出発すると座席がほとんど空いてしまい、何人か集めて出発すると一度に多くの人を運べます。その代わり、集めている間、最初に来た乗客は待たされることになります。 バッチ処理とは、スループットを得るために遅延を犠牲にするトレードオフです。そして、バスがすでに満員である場合(1人だけで計算ユニットを使い切るモデルである場合)、さらに集めても得られるものはありません。
2. サーバー側の研究:遅延目標内でのバッチ化
- Clipper(Crankshaw他、2017) は、モデルごとに「遅延目標を超えない最大のバッチ」を自動的に見つけます。目標を超えるまではバッチを少しずつ大きくし、超えた場合は10%小さくします(AIMD)。20msの目標下でスループットが最大26倍に向上しましたが、これは CPUの線形SVM という非常に軽量なモデルでの値です(セクション4.3)。バッチが満たされるまで2ms待つ「遅延バッチ処理」は、同じモデルでスループットを3.3倍に向上させましたが、もともと小さなバッチでも効率的なモデルではメリットがありませんでした。
- Clockwork(Gujarati他、2020) は逆側を見ます。DNNの1回の推論時間は本来非常に一定であり、データセンターGPUにおけるバッチサイズ1の99.99パーセンタイルは中央値の0.03%以内でした(1,100万回、セクション2)。テール遅延を引き起こしていたのは、複数の推論を同時に実行するような上位レイヤーの選択であり、同時実行はスループットを最大25%向上させる一方でテール遅延を100倍に拡大させました。そのため、GPUごとに 一度に1つだけ を実行し、中央でスケジュールを管理することで、100msの目標を実質的にすべて満たしました(学会発表ページ基準ではリクエストの99.997%、arXiv版の抽象では99.9999%と異なって記載されています)。
- NVIDIA Tritonの動的バッチ機能はこれを設定ノブとして提供します。デフォルトでは待たずに「今キューにある分だけ」最大バッチまでまとめ、
max_queue_delay_microsecondsを指定すると、バッチを満たすためにその時間まで待ちます。ドキュメントでは、「最大バッチを決定する → デフォルトで有効にする → 測定して遅延予算内であれば大きくするか待ち時間を増やす」という順序を推奨しています。
これら3つをまとめると原則が見えてきます。 遅延予算が最初に決定され、バッチはその予算内でのみ大きくする。そして、どれほど効果があるかは、そのモデルとGPUで実際に測定してみなければ分からない。
3. 実際に測定してみる:空モデルはバッチ化すると高速化するのか
TT Scoreの空モデル(BlurBall、3フレームウィンドウ、パラメータ149万個)を実験用GPUでバッチサイズのみを変更して測定しました(512×288、ウォームアップ10回後、30回×3セットの中央値、cuDNN自動チューニング有効)。
| バッチ | FP16 1回 | FP16 秒間フレーム | FP32 1回 | FP32 秒間フレーム |
|---|---|---|---|---|
| 1 | 9.82ms | 101.8 | 15.11ms | 66.2 |
| 2 | 20.36ms | 98.2 | 34.21ms | 58.5 |
| 3 | 32.71ms | 91.7 | 57.10ms | 52.5 |
| 4 | 46.15ms | 86.7 | 79.69ms | 50.2 |
| 8 | 103.77ms | 77.1 | 178.09ms | 44.9 |
| 16 | 221.25ms | 72.3 | 366.10ms | 43.7 |
まとめるほどスループットが低下しました。 バッチ1がすでにこのGPUを使い切っているため、まとめると中間テンソルが大きくなり(512×288解像度を最後まで維持するモデルであるため、FP32の特徴マップ1つが数十MB)、キャッシュからあふれたものと推測されます(原因は推定)。長時間実行してGPUが熱くなって遅くなったわけでもありません。最も負荷の高い1024×576のバッチ4を数分間実行した後に再度測定したバッチ1は、最初の値と同じでした(9.77対9.82ms)。
入力を256×144に縮小すると、結果は逆になります。
| 256×144 | バッチ1 | バッチ4 | フレームあたり(バッチ4) |
|---|---|---|---|
| FP32 | 4.07ms | 15.40ms | 3.85ms(0.95倍) |
| FP16 | 4.69ms | 9.29ms | 2.32ms(0.49倍) |
小さな入力単体ではGPUに余裕があるため、4つをまとめることでFP16におけるフレームあたりの時間が半分になりました。同じモデルであっても、 入力サイズとGPUによってバッチの得失が逆転します。 2026-10-12にサーバーGPU(24GBクラス)で同じモデルを測定した際は、バッチ3がフレームあたり0.83倍(FP16 8.0ms → バッチ3 20.0ms)とわずかにメリットがありました(TT Scoreパフォーマンスドキュメント 2.9.8)。
4. 複数のセッションが1つのGPUを使用する場合
リアルタイムワーカーは、「リクエスト」ではなく「セッション」を受け取ります。セッションごとに30fpsでフレームが到着し、ワーカーはセッションごとに最新のフレームのみを保持します(第1回の「最新のみ」)。GPUが空いたら、待機中のセッションの最新ウィンドウを すべて1つのバッチにして 処理するのがTT Scoreワーカー方式です。上記の実測テーブルを代入して、セッション数ごとにシミュレートしました(GPUの割り当て分のみ、30秒)。
| セッション | 個別処理(最も古いフレームを優先) | バッチ処理(待機中のセッションすべて) |
|---|---|---|
| ノートPC GPU実測テーブル、セッション3 | セッションあたり30.0fps、遅延中央値9.8ms | 同等 |
| ノートPC GPU、セッション4 | 平均25.5fps(最も遅いセッション18.6)、中央値37.0・p99 43.0ms | すべて21.7fps、中央値62.7・p99 79.1ms |
| ノートPC GPU、セッション6 | 平均17.0fps(最も遅いセッション2.5)、中央値39.3ms | すべて13.5fps、中央値91.4ms |
| サーバーGPUテーブル、セッション5 | 平均25.0fps(最も遅いセッション5.1)、中央値36.0ms | すべて30.0fps、中央値27.4・p99 40.0ms |
| サーバーGPU、セッション6 | 平均20.8fps(最も遅いセッション3.4) | すべて26.3fps、中央値54.7ms |
(code/06_batching_sim.out.txt(サーバーGPUテーブルはバッチ1・3の2点のみであるため、バッチ4以上は直線的に増加すると仮定しています。)
読み方は以下の通りです。
- GPUに余裕がある場合(セッション3以下)は、バッチ化しても違いはありません。 フレームが同時に到着することが稀であるため、バッチがほとんど発生しません。
- GPUが不足している場合、バッチ化は公平性を生み出します。 「最も長く待ったフレームを優先する」方式は上書きが発生するため、フレーム時刻が常に新しいセッションに後回しにされ続け、特定のセッションが2~5fpsで飢餓状態に陥ります。バッチ化すると、すべてのセッションが同じfpsを受け取ることができます。
- スループットはバッチ曲線に従います。 まとめてメリットがあるGPU(サーバーGPUテーブル)では、セッション5をすべて30.0fpsで処理できましたが、メリットがないGPU(ノートPC GPU)ではまとめるほど合計値が低下し、遅延が増加しました。
実際のワーカーの負荷試験(サーバーGPU、他の作業とカードを共有していた2026-10-11時点)も同様の傾向でした。単一セッションでは29.4fps・往復p99 32msであり、3セッションではセッションごとに16~20fps・往復中央値70~95msとなり、送信されたフレームの21~30%が新しいフレームによって上書きされて破棄されました(パフォーマンスドキュメント 4.2)。
5. GPUを共有する方法
複数のタスクが1枚のGPUを共有する方法には3つあります(NVIDIA GPU Operator・デバイスプラグインのドキュメント)。
| 方法 | 共有方法 | メモリ分離 | エラー分離 |
|---|---|---|---|
| 時分割(Time-Slicing) | 交互に使用する。リソースが複数あるように「見せる」だけ | なし | なし |
| MPS | 同時に実行、クライアントごとにメモリ・演算リソースの上限を設定 | 上限あり | ドキュメントで確認できず |
| MIG(対応GPUのみ) | ハードウェアレベルで分割し、インスタンスごとに専用メモリを持つ | あり | あり |
時分割は手軽に見えますが、同じカード上の他のタスクが忙しいと、自身の遅延がそのまま増加します。Clockworkが指摘した「同時実行がテール遅延を拡大させる」という現象が、運用時にも発生している模様です。TT Scoreの実測値でも、同じ空モデルの1フレームあたりの時間は、カードがアイドル状態のときは10.5ms、他のタスク2~3個と共用しているときは34msでした(パフォーマンスドキュメント 2.9.8)。 リアルタイム遅延は、モデルそのものよりも「誰とカードを共用しているか」によって大きく左右されます。
6. 制限事項
- バッチ測定は1種類のGPU、1つのモデルを対象としています。スループットが低下した原因(キャッシュあふれ)は推測であり、プロファイラーによる確認は行っていません。
- セッションのシミュレーションには、ネットワーク、JPEGデコード、後処理が含まれていません。実際のワーカーはそれらを別スレッドで処理するため、GPU割り当て分がボトルネックとなる場合の傾向は同じですが、数値は異なります。
- 「最も古いフレームを優先する」方式における飢餓状態は、このシミュレーションのルールに起因するものです。セッションを順番に巡回するルール(ラウンドロビン)であれば飢餓状態には陥りません。バッチ処理との比較は行っていません(未測定)。
7. 我々のプロジェクトとのつながり
- リアルタイムワーカーは、「待機中セッションの最新フレームをすべてまとめる」ことと「遅延したフレームを上書きする」ことを組み合わせて使用しています。バッチ処理によるスループットのメリットが小さくても公平性が得られ、上書きによって遅延の上限が守られます。同時セッションの上限(3)も、その遅延予算から導き出されたものです。
- セクション3の結果は、「同じコードであってもカードが変更されればバッチポリシーの得失が逆転する」という警告です。ワーカーを別のカードに移行する場合は、バッチ曲線から測り直す必要があります。
- 時分割によってTTSなどの他のタスクとカードを共有している限り、3セッションすべてで30fpsを保証することはできません。この判断(スロット数を減らすか、カードを分けるか)は、数値を添えてパフォーマンスドキュメントに記録してあります。
次の記事は、サーバーではなく スマホ内 で実行するアプローチについてです。
ティティの一言。 バスがすでに満員なのにさらに人を乗せようとすると、ドアが閉まるのが遅くなるだけです。私の空モデルがまさにそれでした――まとめればまとめるほど遅くなったんですから!
参考文献
- Crankshaw, D. et al. (2017). Clipper: A Low-Latency Online Prediction Serving System. NSDI 2017. https://arxiv.org/abs/1612.03079
- Gujarati, A. et al. (2020). Serving DNNs like Clockwork: Performance Predictability from the Bottom Up. OSDI 2020. https://arxiv.org/abs/2006.02464
- NVIDIA Triton Inference Server — Dynamic Batcher. https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/user_guide/batcher.html
- NVIDIA GPU Operator — Time-Slicing GPUs in Kubernetes. https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-sharing.html
- NVIDIA k8s-device-plugin README(Shared Access to GPUs). https://github.com/NVIDIA/k8s-device-plugin
- 私たちの測定: 本シリーズの実測、リポジトリ
docs/tt/PERFORMANCE.md2.9.8·4.2 - サンプルコードと出力: このフォルダの
code/06_batching_sim.py,code/06_batching_sim.out.txt