8月4日、Bala Priya Cが「Static vs. Dynamic vs. Continuous Batching in LLM Inference」と題した記事を公開した。LLM推論のサービングコストが高騰するなか、GPUをいかに遊ばせないかは実運用上の死活問題となっており、バッチング戦略の選択はその中心的なテーマだ。
ChatGPTの普及以降、LLMをAPIとして提供するコストは多くの企業にとって無視できない規模になっている。GPU単価の高さもさることながら、リクエストが来るたびにGPUが遊ぶ時間が積み重なることで、実効的なGPU稼働率は思いのほか低くなりがちだ。単一リクエストを逐次処理する構成では、モデルの重みをGPUメモリからロードするコストをリクエストごとに払い続けることになる。
特にLLMでは、あるリクエストが5トークンで終わり、別のリクエストが500トークンかかるといった出力長の非均一性が問題をさらに悪化させる。バッチング(複数リクエストをまとめて一度に処理する手法)はこの問題への直接的な答えだ。ただし「どう束ねるか」の設計次第で、性能は大きく変わる。
Static Batching:シンプルだが待ち時間が問題
静的バッチングは最も単純な実装だ。設定したバッチサイズ(例:8件)のリクエストが揃うまで処理を待ち、揃ったら一度にまとめてforward passを実行する。
モデルの重みをGPUメモリからロードするコストは高い。それをk回のリクエストに対して1回で済ませられるのが、静的バッチングの効率的な側面だ。大規模なデータセットへのオフラインバッチ推論のような、レイテンシを気にしないユースケースでは有効に機能する。
しかし、リアルタイムトラフィックには根本的に向いていない:
- 最初に到着したリクエストは、残りのスロットが埋まるまで待機させられる
- バッチ内の全リクエストが、最も遅いリクエストが終わるまで処理結果を受け取れない
- 待ち時間の上限がなく、レイテンシ要件のある用途には使えない
Dynamic Batching:タイムアウトで最悪ケースを抑える
動的バッチングは静的バッチングの「バッチが満杯になるまで待つ」という制約を取り除く。代わりに最大バッチサイズとタイムアウト時間の2つの上限を設け、どちらか先に達した時点でバッチを実行する。
タイムアウトの設定がトレードオフの核心だ:
- 短いタイムアウト → レイテンシを守れるが、バッチが小さくなりGPU効率は落ちる
- 長いタイムアウト → バッチが埋まりやすくスループットは上がるが、早着リクエストの待ち時間が増える
NVIDIAのTriton推論サーバーのベンチマークでは、適切にチューニングした動的バッチング設定がスループットを大幅に改善した一方、テールレイテンシは若干増加したと報告されている。元記事では具体的な数値の記載がなく定性的な評価にとどまっているが、これは動的バッチングの典型的なトレードオフを示す事例として紹介されている。
ただし、動的バッチングもバッチが走り始めたら最後まで止まれない点は静的バッチングと同じだ。バッチ内に長いリクエストが1件混じれば、他の短いリクエストはそれを待たされる。画像生成モデルのように出力長がほぼ均一な場合は問題にならないが、LLMには致命的な欠点となる。
Continuous Batching:トークン単位でスケジューリングする
連続バッチングはスケジューリングの単位を「リクエスト」から「個別のデコードステップ(1トークン生成)」に変える。これがLLM推論において質的に異なるアプローチだ。
仕組みはこうだ。サーバーはアクティブな全シーケンスについて、1ステップごとにforward passを実行して各シーケンスの次のトークンを生成する。あるシーケンスが終端トークン(EOS)を出力した瞬間、そのスロットは解放され、次のイテレーションで即座に新しいリクエストが挿入される。固定のバッチが「完了」するまで待つ必要がない。アクティブなシーケンスの集合はほぼ毎ステップ入れ替わり、GPUはほとんど待機しない。
チャンクドprefillとは
連続バッチングには一つの課題がある。新しいリクエストが到着した際、プロンプト全体を一度にエンコードするprefillフェーズは、デコードステップと比べて計算コストが格段に高い。長いプロンプトのprefillが走ると、その間アクティブな他のシーケンスのデコードが止まり、レイテンシが跳ね上がる「プリフィルスパイク」が起きる。
これに対処するのがチャンクドprefillだ。長いプロンプトをあらかじめ固定サイズのチャンク(例:512トークン単位)に分割し、複数のデコードステップにまたがって少しずつ処理する。これにより、prefill中も他シーケンスのデコードを継続でき、テールレイテンシの悪化を抑えられる。vLLMではこの機能を--enable-chunked-prefillフラグで有効化できる。
vLLM、TensorRT-LLM(「in-flight batching」という名称)、TGI(Text Generation Inference)など、主要なLLM推論フレームワークは連続バッチングをデフォルトとして採用している。高負荷時のスループットは動的バッチングを大幅に上回る。一方、トラフィックが軽い状況では動的バッチングの方がTime to First Token(最初のトークンが返るまでの時間)が短くなる場合もある。
3手法の比較まとめ
| 手法 | スケジューリング単位 | GPUアイドル | レイテンシ特性 | 適したユースケース |
|---|---|---|---|---|
| Static Batching | バッチ全体 | 高い | バッチ内最遅リクエストに依存 | オフラインバッチ処理 |
| Dynamic Batching | タイムアウト付きバッチ全体 | 中程度 | バッチサイズ・タイムアウトに依存 | 画像生成など出力長が均一なモデル |
| Continuous Batching | 個別デコードステップ | 低い | リクエスト毎に変動・総スループットは高 | 本番LLMサービング |
LLM推論の最適化はバッチング戦略だけでなく、メモリ管理とも深く絡み合う。代表例が**PagedAttention**(vLLMが提案した手法で、KVキャッシュをOSの仮想メモリのページング方式で管理することでメモリ断片化を抑制する)だ。連続バッチングはPagedAttentionと組み合わせることで初めて実用的なスループットを発揮できる設計になっており、両者はセットで理解しておくべき概念だ。まずバッチング戦略の違いを正確に理解することが、推論スタックを選定・チューニングする際の前提知識となる。
詳細はStatic vs. Dynamic vs. Continuous Batching in LLM Inferenceを参照していただきたい。