Q9. バッチ処理とスループット
複数リクエストをまとめて処理するContinuous Batchingを実際に使ったことはありますか? スループットを上げるためにどんな調整をして、どんな問題が出ましたか?
出典: @voidwarriorchan の投稿(2026-08-25)
- 普通の「まとめて処理するバッチ」が、LLM(大規模言語モデル)のサービング(推論を外部に提供する処理)ではなぜそのまま使えないのか
- Continuous Batching(継続的バッチ処理)が「1トークン生成するたびにバッチの中身を入れ替える」という発想でその問題を解くしくみ
- 同時に処理できるリクエスト数の天井を最終的に決めているのが何で、それをどう調整するとスループットとレイテンシがどうトレードオフするか
- 実際に調整すると何が起きるか(メモリ枯渇による退避、他リクエストの割り込みによる生成の停滞)を具体的に語れる回答と、「バッチを増やせば増やすほど良い」で終わる弱い回答の違い
この質問は何を聞いているのか
質問文の「Continuous Batching」(コンティニュアス・バッチング、継続的バッチ処理)は、複数ユーザーのリクエストを1回のGPU(並列計算に特化したチップ)計算にまとめて乗せる「バッチ処理」の一種で、まとめる単位を固定バッチではなく1イテレーション(モデルの全層を1回通す処理)ごとに組み替え続ける方式を指す。LLMの生成は自己回帰生成(=直前までに出したトークンを入力に加えて次の1トークンを作る、を繰り返す生成方式)なので、1イテレーションごとに出力トークンが1個増える。「スループット」(throughput、単位時間あたりに処理できる仕事量。ここでは1秒あたりに捌けるリクエスト数やトークン数)は、1件あたりの応答時間である「レイテンシ」とトレードオフの関係にある。
つまりこの質問は、「バッチ処理」という言葉を知っているかではなく、①なぜLLMのサービングだけ普通のバッチ処理と違う工夫が要るのか、②その工夫が具体的にどう動くのか、③実際に同時実行数やメモリの設定値を動かして、性能が上がった経験だけでなく副作用が出た経験まで持っているか、を測っている。「使ったことがある」で終わらず、何を調整して何が犠牲になったかを具体的に語れるかが分かれ目になる。
なぜ普通のバッチ処理では足りないのか
複数のリクエストをGPUに1回でまとめて計算させる「バッチ処理」自体は目新しい発想ではない。問題は、LLMの生成が「何トークンで終わるか事前に分からない」自己回帰処理だという点にある。従来型のバッチ処理(静的バッチ、static batching)は、バッチに入れるリクエストの集合をいったん決めたら、全員の生成が終わるまでそのバッチを固定する。オープンソース推論サーバーTGI(Text Generation Inference)の設計文書はこれを「クライアントが事前にバッチ量を決めてしまう方式」と表現する。
ここで問題が起きる。4件のリクエストを1つのバッチに詰めたとき、生成する長さは人によって違う。1件が先に終わっても、バッチは全員が終わるまで解散しない。この静的バッチの限界を最初に整理したOrca論文(USENIX OSDI '22)は3つの無駄を指摘する。①早く終わったリクエストの分もバッチの形を保つために計算し続けてしまう、②終わったリクエストの結果をすぐにクライアントへ返せない、③新しく届いたリクエストはバッチ全体が終わるまで順番待ちする。GPUの計算枠が空いているのに、誰もそこに入れない状態が続く。
なぜ長さも段階もバラバラな要求を1回の計算にまとめられるのか
ここで素朴な疑問が出る。図7-1下段のように枠の中身を毎回入れ替えるなら、ある瞬間のバッチには「入力を読み込んでいる最中の要求」と「すでに出力を生成している要求」が混在する。GPUの行列演算は本来、同じ形をそろえたデータでないとまとめて計算できない。
答えは「演算の種類によって扱いを変える」ことだ。Attention(注意機構。各トークンが過去のどのトークンに注目するかを計算する層)以外の演算——重み(学習済みパラメータ)を掛けるLinear層や正規化——は、どの要求の何トークン目かを気にせず、全要求のトークンを1本の列にただ並べて処理できる。4件の要求から合計7トークンが来ていれば、形状 [7, H](Hは1トークンあたりの内部表現の次元数)の1本のテンソル(=GPUが一括で計算する多次元の数値の配列)としてまとめて計算すればよい。Attentionだけは別で、各トークンは自分の要求の過去のトークン(KVキャッシュ、key-valueキャッシュ。読み込んだトークンごとの中間データをGPUメモリに取り置き、次のトークンを作るたびに計算し直さない仕組み)としか照合してはいけないため、要求ごとに一度切り分けて計算し、終わったら再び1本の列に戻す。Orca論文はこの解法を「selective batching」(選択的バッチ処理)と呼ぶ。
この使い分けが成立する理由をOrca論文は明快に説明する。Attentionは学習済みパラメータを持たない演算なので、複数要求をまとめて計算しても「重みをメモリから1回読んでみんなで使い回す」という省エネ効果が得られない。要求ごとに分けて計算しても損が小さいということだ。逆にLinear層のような重みを持つ演算は、まとめて処理するほど「重みを1回読み出して多くのトークンで使い回せる」ため、まとめる価値が大きい。読み込み段階(prefill)と書き出し段階(decode)が混ざっても1つのバッチとして扱えるのは、この演算単位での使い分けがあるからだ。
同時実行数の天井はKVキャッシュが決める
継続的バッチ処理は「枠が空いたら次を入れる」方式だが、そもそも枠の数——同時に処理できるリクエスト数——には上限がある。上限を決めているのはGPUの計算力ではなく、KVキャッシュを置くためのメモリ容量だ。KVキャッシュは要求ごとに生成が進むほど線形に増え続けるため、GPUメモリのうちモデルの重みを載せた残りの空き容量を、1要求あたりのKVキャッシュサイズで割った数が、同時に走れる要求数の理論上限になる。
この上限を初期のOrcaは「要求ごとに最大生成トークン数分のメモリを最初から丸ごと確保する」方式で管理していた。ところが実際の生成長は最大値よりずっと短いことが多く、vLLM(オープンソースのLLM推論サーバー)の論文(SOSP 2023)の実測では、この方式で確保したKVキャッシュのうち実データに使われていたのはわずか20.4〜38.2%だった。残りは「確保はしたが使われない」内部断片化と、要求ごとに確保サイズが違うことで生じる外部断片化で無駄になっていた。
vLLMが導入したPagedAttention(ページドアテンション)は、OS(オペレーティングシステム)の仮想メモリのページングにヒントを得て、KVキャッシュを固定サイズの小さなブロック単位で管理する。要求は必要な分だけブロックを積み増していけばよく、ブロックはどこにあってもよい(非連続でよい)ので、無駄が出るのは最後の1ブロックだけになる。この設計により、同じメモリでより多くの要求を同時に走らせられ、結果としてスループットが上がる。
調整のダイヤルと、そこで起きる問題
継続的バッチ処理を実際に運用する自前ホスティング(vLLM・TGI・TensorRT-LLM〈NVIDIA製の推論ライブラリ〉など)では、主に3種類のダイヤルを調整する。①同時に走らせるリクエスト数の上限(vLLMのmax_num_seqs、TensorRT-LLMのmax_batch_size)、②1イテレーションあたりに処理するトークン数の予算(vLLMのmax_num_batched_tokens、TensorRT-LLMのmax_num_tokens)、③KVキャッシュに割り当てるGPUメモリの割合(vLLMのgpu_memory_utilization、既定0.92)だ。Orca論文は①の同時実行数(max batch size)を上げるほどスループットは伸びるがレイテンシも増え、その伸びは収穫逓減すると明記している。実際、GPT-3 175Bでの評価では、生成トークン1個あたりに正規化したレイテンシ(normalized latency)の中央値を190ミリ秒にそろえた条件で、従来方式(FasterTransformer。NVIDIAの旧世代推論ライブラリで、静的バッチと同じくリクエスト単位でバッチを固定する)が毎秒0.185リクエストしか捌けなかったのに対し、Orcaは毎秒6.81リクエストを捌いた(36.9倍)。②のトークン予算と③のメモリ割合には別のトレードオフがある。vLLM公式ドキュメントはmax_num_batched_tokensを小さくするとITL(トークン同士の間隔)が改善し、大きくするとTTFT(最初のトークンが出るまでの時間)が改善すると説明する。TensorRT-LLM公式のチューニングガイドは、max_num_tokensを上げすぎるとKVキャッシュ用のメモリが減り、長いコンテキストではOOM(メモリ不足によるエラー)につながりうると注意している。TensorRT-LLM公式のチューニング実測(Llama-3.3-70B、4基のH100〈NVIDIAのデータセンター向けGPU〉)でも、max_batch_sizeを64→512に上げるとトークンスループットが約20%改善する一方、512→2048までさらに上げるとかえって悪化する例が示されている。上げれば上げるほど良い、という単純な話ではない。
| 調整項目 | vLLM | TGI | TensorRT-LLM |
|---|---|---|---|
| 同時実行数の上限 | max_num_seqs(既定256〜1024、GPU世代依存) | --max-concurrent-requests(既定128) | max_batch_size(既定2048) |
| 1イテレーションのトークン予算 | max_num_batched_tokens(既定2048〜16384) | --max-batch-total-tokens(自動推定) | max_num_tokens(既定8192) |
| KVキャッシュに使うメモリ割合 | gpu_memory_utilization(既定0.92) | 該当する割合指定なし。KV予算は--max-batch-total-tokens(自動推定、行2と同じ)が兼ねる。--cuda-memory-fraction(既定1.0)はCUDAメモリ全体の上限 | kv_cache_free_gpu_mem_fraction(既定0.90) |
| KV枯渇時の挙動 | RECOMPUTE方式で退避(既定) | --max-batch-total-tokensの予算内でしか新規要求を入れない(枯渇させない設計)。同時接続数が--max-concurrent-requestsを超えるとoverloadedエラーで拒否 | GUARANTEED_NO_EVICT(既定)/MAX_UTILIZATION |
既定値はいずれも2026-08時点の公式ドキュメント・ソースの値。vLLMの既定はGPU世代と起動方法(vllm serveかLLM()か)で変わる。
③のメモリ割合を欲張りすぎる、あるいは①②の予算を大きくしすぎると、必ず起きる問題が「KVキャッシュの枯渇」だ。稼働中のすべての要求にKVキャッシュを積み続けた結果、次の1トークンを置く空きブロックがなくなる瞬間が来る。vLLMはこのとき最新到着(=バッチの中で優先度が最も低い)の要求を選んで退避(プリエンプション、preemption)させ、待ち行列に戻す。空きができて再スケジュールされると、その要求は入力とそれまでの生成分をまとめて改めてprefillからやり直す(再計算、recompute)。TensorRT-LLMは既定でこれをそもそも起こさない方針(GUARANTEED_NO_EVICT、一度始めた要求は止めない)を取り、スループット最優先のMAX_UTILIZATIONに切り替えると同じリスクを背負う。TGIはKV枯渇後の退避ではなく、--max-batch-total-tokensというトークン予算の残りに収まる分だけ新規要求をバッチへ入れる。予算とは別に、同時接続数が--max-concurrent-requestsを超えた要求はoverloadedエラーで突き返す(backpressure)。vLLMとTensorRT-LLMは枯渇が起きる前提で退避や利用制限の設計を持つ一方、TGIはトークン予算の範囲内でしか新規要求を入れないことで枯渇そのものを防ぐ設計になっている。
もう1つ現場で必ず出る問題が、長いプロンプトの読み込み(prefill)が進行中の生成(decode)を数秒止めてしまう「生成の停滞」だ(詳しい仕組みは第5章参照)。継続的バッチ処理にすると、稼働中の要求のdecodeと、新しく届いた要求の巨大なprefillが同じイテレーションに混ざり得る。prefillを丸ごと1回で処理すると、その間ずっと他の要求のトークンが1つも進まない。対処は②のトークン予算を絞ってprefillを小分けにするchunked prefill(第5章参照)で、Sarathi-Serve論文はこれを使わない場合、decode専用バッチに比べてトークン間隔(レイテンシ指標の1つ)が最大28.3倍に悪化しうると報告している。
実務では何をするのか
Anthropic・OpenAIのようなホスト型APIでは、①〜③に相当する推論サーバーの設定項目は利用者に公開されていない。したがって、この質問に「自分で調整した経験」で答えられるのは、vLLM・TGI・TensorRT-LLMなどで自前でモデルをホスティングした場合に限られる。API利用者の側で関係するのは、同時リクエスト数とTTFT・ITLの計測(第5章参照)までである。
自前ホスティングでの実務は次の順で進む。まず①同時実行数と②トークン予算をハードウェアの既定値から出発し、負荷試験ツール(vLLMならvllm bench serve)でTTFT(最初のトークンが出るまでの時間)・ITL(トークン同士の間隔)・スループットを計測する。次に③のメモリ割合を上げてKVキャッシュの余裕を確保し、退避(プリエンプション)の発生頻度を監視する。vLLMは退避が起きるたびに「Sequence group 0 is preempted by PreemptionMode.RECOMPUTE mode because there is not enough KV cache space」という警告をログへ出し、頻発するなら公式ドキュメントはgpu_memory_utilizationを上げる、max_num_seqsやmax_num_batched_tokensを下げる、あるいはテンソル並列数(tensor_parallel_size。1つのモデルを複数のGPUに分割して載せる枚数。増やすとGPU 1枚あたりに載る重みが減り、KVキャッシュに使える空きメモリが増える)を増やす、のいずれかを勧めている。
運用中に見るべき指標は、待ち行列の長さ(vLLMのvllm:num_requests_waiting)、KVキャッシュの使用率(vllm:kv_cache_usage_perc)、退避の累積回数(vllm:num_preemptions)の3つが中心になる。待ち行列が伸びていれば同時実行数の上限か複製(同じモデルを載せたサーバーの追加)が足りず、KVキャッシュ使用率が高止まりしていれば退避の予兆、退避回数そのものが増え続けていれば③のメモリ割当か①②の予算を見直す合図だ。
回答を見極めるポイント
- 静的バッチ(リクエスト単位でスケジュールし全員が終わるまで固定)と継続的バッチ処理(イテレーション単位で終わった要求を抜き新着を入れる)の粒度の違いを説明できる
- なぜ長さの違う要求を1つの演算にまとめられるかを説明できる(Attentionだけ要求ごとに分離し、それ以外はトークン単位で平坦化する)
- 同時実行数の天井がKVキャッシュメモリで決まることを理解し、
max_num_seqsやgpu_memory_utilizationなど具体的なパラメータ名を挙げられる - スループットとレイテンシがトレードオフであり、バッチを増やすほど収穫逓減する(上げすぎるとむしろ悪化する場合もある)ことを定量感を持って語れる
- 実際に起きた問題を具体的に言える(KV枯渇による退避、長いプロンプトの割り込みによる生成の停滞など)
- 自分が監視した指標を挙げられる(待ち行列長・KVキャッシュ使用率・退避回数など)
- 「バッチサイズを上げれば上げるほどスループットが上がる」と言う(収穫逓減があり、実測でも上げすぎるとかえって悪化する例がある)
- 「継続的バッチ処理は静的バッチより常に速くてレイテンシも良い」と言う(静的バッチも最大スループット自体には届き得る。継続的バッチ処理はスループットとレイテンシの折衷点を探すものであって、両方が無条件に良くなる魔法ではない)
- 「継続的バッチ処理を使えばメモリ不足は起きない」と言う(各実装はKV枯渇が起きる前提で退避やエラー応答の設計を持っている)
- 用語を言い換えるだけで、なぜAttentionだけ特別扱いされるのかなど、仕組みの理由を説明できない
深掘り質問の例
- 同時実行数の上限を上げたとき、TTFTとITLはそれぞれどう変化しましたか?
- KVキャッシュが枯渇したことにどうやって気づきましたか? ログや指標で何を見ましたか?
- 静的バッチと継続的バッチ処理を両方試したことがあれば、何を比較しましたか?
つまずきポイント
「同時実行数の上限は大きいほど得」と思い込み、際限なく上げようとする。実際はKVキャッシュが枯渇して退避が多発し、かえって全体のレイテンシが悪化する。
「継続的バッチ処理にすれば1人あたりの応答も速くなる」と誤解している。上がるのはシステム全体のスループットであって、個々のリクエストの応答時間はむしろ負荷が高いほど延びうる(TGIは「レイテンシ2倍でユーザー10倍」を許容できるトレードオフと表現する)。
KVキャッシュ枯渇後の「退避」をシステム障害だと誤解して慌てる。設計どおりに動作する仕組みであり、頻発するならパラメータ(メモリ割当・同時実行数・トークン予算)で対処すべき合図として読む。
長いプロンプトが届くたびに他の全員の生成が止まる「生成の停滞」に気づかず、原因不明の遅延スパイクとして扱ってしまう。トークン予算を絞るchunked prefillで緩和できる問題だと知らない。
理解度チェック
Q1. 静的バッチでは、1件のリクエストが早く終わってもGPUの計算枠はすぐに空きませんか? なぜですか?
空きません。静的バッチはバッチ全体を固定して全員が終わるまで解散しないため、空き枠があっても新しいリクエストをすぐには入れられずGPUの計算資源が遊んでしまう。
Q2. prefill中の要求とdecode中の要求を同じバッチに混ぜて計算できるのはなぜですか?
Attention以外の演算(Linear層など)はどの要求の何トークン目かを問わずトークン単位で1本の列にまとめて処理できるため。Attentionだけは要求ごとに切り分けて自分のKVキャッシュを参照する(selective batching)。
Q3. 同時に処理できるリクエスト数の天井を最終的に決めているのは何ですか?
GPUメモリに載せられるKVキャッシュの容量。モデルの重みを載せた残りのメモリを、1要求あたりのKVキャッシュサイズで割った数が理論上限になる。
Q4. 同時実行数の上限を上げ続けると最終的に何が起きますか?
最初はスループットが伸びるが伸び方は鈍り(収穫逓減)、やがてKVキャッシュが枯渇すると要求が退避(プリエンプション)されて再計算が発生し、かえってレイテンシが悪化する。
まとめ
- 静的バッチはバッチ全体を固定するため、早く終わった要求の枠を解放できず、新着要求を待たせてGPUを遊ばせてしまう
- 継続的バッチ処理はイテレーション単位で枠の中身を組み替え、終わった要求を抜いた瞬間に新着を入れる
- 混在バッチが成立するのは、Attentionだけ要求ごとに分離し、それ以外はトークン単位で1本にまとめて処理する「selective batching」の設計があるため
- 同時実行数の天井はKVキャッシュメモリの容量が決める。PagedAttentionはこのメモリの無駄(内部・外部断片化)を最後の1ブロックまで削る
- 同時実行数・トークン予算・メモリ割当のダイヤルはスループットとレイテンシのトレードオフを作り、上げすぎるとKV枯渇による退避や生成の停滞という具体的な問題が起きる
次章では、モデルが関数呼び出しの結果をどこまで信頼してよいか、実行前の検証と失敗時の対処という運用の問題を取り上げる。
参考リンク
- Yu et al. — Orca: A Distributed Serving System for Transformer-Based Generative Models(USENIX OSDI '22)
- Kwon et al. — Efficient Memory Management for Large Language Model Serving with PagedAttention(arXiv:2309.06180)
- Agrawal et al. — Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve(arXiv:2403.02310)
- vLLM — Optimization and Tuning
- vLLM — Metrics Design
- Hugging Face TGI — Router README(Continuous Batching)
- NVIDIA TensorRT-LLM — In-flight Batching and Paged Attention
- NVIDIA TensorRT-LLM — Tuning max_batch_size and max_num_tokens