「AI 開発してます」を見極める 10 の質問 — 素人のための解説|2026-08 版
第 7 章Q9

Q9 バッチ処理とスループット

乗り合いバスを途中で乗り降り自由にする — Continuous Batching の発想

Q9. バッチ処理とスループット

複数リクエストをまとめて処理するContinuous Batchingを実際に使ったことはありますか? スループットを上げるためにどんな調整をして、どんな問題が出ましたか?

出典: @voidwarriorchan の投稿(2026-08-25)
この章で分かること

この質問は何を聞いているのか

質問文の「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 2 3 4 要求1 要求2 要求3 要求4 新着 要求5 待機中 (バッチ終了後に開始) 終了済みの枠(凡例の薄い色)があっても、要求5はバッチ全体が 終わる(要求3が終わる)まで入れない 継続的バッチ処理:1イテレーションごとに枠を入れ替え イテレーション 1 2 3 4 枠1 枠2 枠3 枠4 要求1 要求1 要求1 要求7 要求2 要求5 要求5 要求5 要求3 要求3 要求3 要求3 要求4 要求4 要求6 要求6 枠2は要求2→要求5、枠4は要求4→要求6と入れ替わる 空きが出た瞬間に次の要求が入るのでGPUが遊ばない
図 7-1 静的バッチ(上)は要求2・要求4が早く終わっても枠を解放できず、新着の要求5は要求1〜4の全員が終わるまで待たされる。継続的バッチ処理(下)はイテレーションごとに枠の中身を組み替え、終わった要求を抜いた瞬間に新着(要求5・6・7)を同じ枠へ入れる。

なぜ長さも段階もバラバラな要求を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つのバッチとして扱えるのは、この演算単位での使い分けがあるからだ。

入力:4件の要求で合計7トークン(Orca論文の例) R1 R2 R3 R4 平坦化(トークン単位で1本の列にまとめる) 形状:[7, H] R1 R2 R3 R3 R4 R4 R4 Attention直前でSplit(要求ごとに分離) 1 1 2 3 過去3 過去1 なし なし Attentionは要求ごとに自分のKVキャッシュだけを参照し、終わったら再びMergeして次の層へ渡す
図 7-2 非Attention演算(Linear・正規化など)はR1〜R4のトークンを1本の列 [7, H] に平坦化してまとめて処理する。Attentionの直前だけ要求ごとの塊(1・1・2・3トークン。R1・R2は書き出し段階、R3・R4は読み込み段階)に分離し、各要求が自分のKVキャッシュだけを参照する。

同時実行数の天井はKVキャッシュが決める

継続的バッチ処理は「枠が空いたら次を入れる」方式だが、そもそも枠の数——同時に処理できるリクエスト数——には上限がある。上限を決めているのはGPUの計算力ではなく、KVキャッシュを置くためのメモリ容量だ。KVキャッシュは要求ごとに生成が進むほど線形に増え続けるため、GPUメモリのうちモデルの重みを載せた残りの空き容量を、1要求あたりのKVキャッシュサイズで割った数が、同時に走れる要求数の理論上限になる。

この上限を初期のOrcaは「要求ごとに最大生成トークン数分のメモリを最初から丸ごと確保する」方式で管理していた。ところが実際の生成長は最大値よりずっと短いことが多く、vLLM(オープンソースのLLM推論サーバー)の論文(SOSP 2023)の実測では、この方式で確保したKVキャッシュのうち実データに使われていたのはわずか20.4〜38.2%だった。残りは「確保はしたが使われない」内部断片化と、要求ごとに確保サイズが違うことで生じる外部断片化で無駄になっていた。

vLLMが導入したPagedAttention(ページドアテンション)は、OS(オペレーティングシステム)の仮想メモリのページングにヒントを得て、KVキャッシュを固定サイズの小さなブロック単位で管理する。要求は必要な分だけブロックを積み増していけばよく、ブロックはどこにあってもよい(非連続でよい)ので、無駄が出るのは最後の1ブロックだけになる。この設計により、同じメモリでより多くの要求を同時に走らせられ、結果としてスループットが上がる。

連続確保:最大生成長ぶんを最初から丸ごと確保 要求1 80%が実データ 要求2 30%が実データ 要求3:20%が実データ、残り80%は確保のみで未使用(内部断片化) vLLM論文の実測:実データに使われたのは確保領域の20.4〜38.2%だけ PagedAttention:固定サイズのブロックを必要な分だけ確保 =実データ =未使用(最後のみ) 要求1 要求2 要求3 空きプール 4ブロック 2ブロック 3ブロック
図 7-3 連続確保(上)は要求ごとに最大長分を丸ごと押さえるため確保領域の大半が未使用のまま眠る(比率は説明用の例。実測値はvLLM論文の20.4〜38.2%〈全体〉)。PagedAttention(下)は固定サイズのブロックを必要な分だけ積み増すので、塗りつぶし部分が実データ、白い部分が未使用を表し、無駄が出るのは各要求の最後の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までさらに上げるとかえって悪化する例が示されている。上げれば上げるほど良い、という単純な話ではない。

調整項目vLLMTGITensorRT-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はトークン予算の範囲内でしか新規要求を入れないことで枯渇そのものを防ぐ設計になっている。

① KVキャッシュが埋まっている ② 最新到着の要求を退避して空きを作る 実行中(Running) 要求1 要求2 要求3 要求4(最新到着) KV使用率100% KV枯渇→退避 実行中(Running) 要求1 要求2 要求3 待機中(Waiting) 要求4(PREEMPTED) 使用率75% 空きができたら要求4を再スケジュール。ただしprefillからやり直しになる(recompute)
図 7-4 KVキャッシュが埋まると、優先度の最も低い要求4が待機列へ戻され(PREEMPTED)、空いたブロックで要求1〜3の生成が続く。要求4は後で再スケジュールされ、入力の読み込み(prefill)からやり直す。使用率の数値(100%・75%)は説明用の例。

もう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キャッシュ使用率が高止まりしていれば退避の予兆、退避回数そのものが増え続けていれば③のメモリ割当か①②の予算を見直す合図だ。

回答を見極めるポイント

良い回答に含まれる要素
弱い回答のサイン

深掘り質問の例

つまずきポイント

「同時実行数の上限は大きいほど得」と思い込み、際限なく上げようとする。実際は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キャッシュが枯渇すると要求が退避(プリエンプション)されて再計算が発生し、かえってレイテンシが悪化する。

まとめ

次章では、モデルが関数呼び出しの結果をどこまで信頼してよいか、実行前の検証と失敗時の対処という運用の問題を取り上げる。

参考リンク