状態:ドラフト(2026-10-13)、一部実測。 論文と公式ドキュメントは2026-10-13に直接アクセスして確認しました。実測値は同サイトの「トス ML/AI Infra 準備」第14回において、8GBクラスのGPU1枚、Qwen3-1.7B、vLLM 0.31.0の環境で測定された値を再引用しています。大規模モデルや複数GPUにおける値は未測定です。 作成ルール:実測データを基本とし、測定できなかったものは「未測定」、確認できなかったものは「未確認」と表示します。
目次
- 1. スループットは結局バッチサイズの問題である
- 2. PagedAttention:KVをブロックに分割する
- 3. 連続バッチ処理:空いたスロットをステップごとに埋める
- 4. プレフィックスキャッシュ:同じ前半部分は1度だけ計算する
- 5. KVの容量が不足する場合
- 6. まとめ:そしてGPUメモリの壁
- 参考
1. スループットは結局バッチサイズの問題である
第1回の結論を再掲します。デコードはトークンを1つ生成するたびに重み全体をメモリから読み込む必要があるため、リクエストを1つだけ処理する場合はGPUの演算器のほとんどが遊んでしまいます。重みを1回読み込む際に複数のリクエストを同時にデコードすれば、同じ読み込みで複数のトークンを生成できます。そのため、 バッチを大きくするとスループットが向上します。
バッチに含まれるリクエストは、それぞれ自身のKVをGPUメモリ上に保持する必要があります。重みを配置した残りのメモリをKVで分割して使用するため、 同じメモリにKVをどれだけ高密度に格納できるかがバッチサイズ、すなわちスループットを決定します。 サービングエンジンのKV管理手法のほとんどは、この1文のために存在しています。
| 手法 | 節約する対象 | スループットに到達するアプローチ |
|---|---|---|
| PagedAttention | 予約や断片化によって無駄になるKVメモリ | 同じメモリにより多くのリクエスト → より大きなバッチ |
| 連続バッチ処理 | 早期終了したリクエストの空きスロット | ステップごとにバッチをフルに満たす |
| チャンクプリフィル | 長いプレフィルがデコードを停止させる時間 | トークン間隔の制約を守りながらバッチを拡大する |
| プレフィックスキャッシュ | 同じ前半部分のKV計算とメモリ | プレフィルの計算をスキップしてブロックを共有する |
2. PagedAttention:KVをブロックに分割する
旧方式の無駄
vLLM以前のサービングシステムでは、リクエストごとに 最大出力長分のKV領域をひと塊としてあらかじめ確保していました。 出力が何トークンになるか分からないため、余裕を持って確保せざるを得ません。vLLMの論文(Kwonら、SOSP 2023)では、ここで発生する無駄を3つに分類しています。
- 予約:今後使用するかもしれない領域をあらかじめ確保しておくこと
- 内部断片化:最後まで使用されないままリクエストが終了した領域
- 外部断片化:塊の間に取り残され、誰も使用できない隙間
論文の測定によると、既存システムは KVメモリの20.4%〜38.2%しか実際のトークン保存に使用していませんでした。 同論文の図によると、A100で13Bモデルをサービングする場合、メモリの約65%が重み、30%近くがKVです。KVの割り当てが本来少ないにもかかわらず、その中の3分の2以上を捨てていることになります。
ブロックとブロックテーブル
PagedAttentionはオペレーティングシステムの仮想記憶のようにこれを解決します。KVを 固定サイズブロック(デフォルト16トークン)に分割し、リクエストごとに「論理ブロック → 物理ブロック」の ブロックテーブルを保持します。ブロックはGPUメモリ内の空き領域のどこにでも配置され、トークンが1つのブロックを埋め尽くすと、その時点で新しいブロックが1つ割り当てられます。
- 塊が存在しないため、 外部断片化が解消されます。
- 事前予約を行わないため、 予約の無駄もありません。
- 残る無駄は、リクエストごとの 最後のブロックの空きスペースのみです(16トークン未満)。
アテンションカーネルはブロックテーブルを辿りながら分散したブロックからK・Vを読み込みます。連続した配列でなくても計算結果は同じです。
ブロックの共有
ブロック単位で管理すると、 複数のリクエストが同じブロックを指すことができます。 1つのプロンプトから複数の回答を生成するパラレルサンプリングやビームサーチでは、プロンプト部分のKVが同一であるため、ブロックに参照カウントを設定して共有します。誰かがそのブロックに追記する必要が生じた場合にのみコピーが行われます(copy-on-write)。論文の測定によると、これによって削減されたKVメモリは以下の通りです。
| データセット | パラレルサンプリング | ビームサーチ |
|---|---|---|
| Alpaca | 6.1~9.8% | 37.6~55.2% |
| ShareGPT | 16.2~30.5% | 44.3~66.3% |
結果
論文では、同一のレイテンシ水準におけるスループットがFasterTransformerやOrcaよりも 2〜4倍 高いと報告されています(OPT-13B・66B・175BおよびLLaMA-13B、A100 40GB 1枚〜A100 80GB 8枚)。ShareGPTワークロードにおいて処理できるリクエストレートはFasterTransformerの最大22倍でした。比較対象やワークロードによって倍率が大きく異なるため、「2〜4倍」を代表値として読み取れば問題ありません。
3. 連続バッチ処理:空いたスロットをステップごとに埋める
メモリを節約しても、バッチを構成する方式が不適切であればスペースが空いてしまいます。リクエストごとに出力長が異なるためです。 静的バッチ処理は複数のリクエストをまとめて開始し、 すべてが終了するまで 新しいリクエストを受け付けません。10トークンの回答が早く終わっても、そのスロットは500トークンの回答が終わるまで遊んだままになります。
Orca(Yuら、OSDI 2022)はスケジュールを モデルの1ステップ(iteration)単位に変更しました。ステップが終了するたびに完了したリクエストを即座に返し、待機していたリクエストを次のステップから投入します。これが現在大部分のエンジンで採用されている 連続バッチ処理です。論文では、GPT-3 175Bクラスのモデル(A100 40GB 16枚)においてトークンあたりのレイテンシ中央値を190msに合わせた際、FasterTransformerが秒間0.185リクエスト、Orcaが6.81リクエストを処理したと報告しています(36.9倍)。この比較の基準線は静的バッチ処理であり、この差には連続バッチ処理だけでなくOrcaの他の設計も含まれています。
連続バッチ処理とPagedAttentionは互いを必要としています。ステップごとにリクエストが出し入れされるにはKV領域をその都度確保・解放する必要がありますが、塊の予約方式では隙間が増えるだけです。ブロック単位の割り当てがそれを可能にしました。
新たな課題:長いプレフィルがデコードを停止させる
連続バッチ処理には図(ナ)のような問題が残ります。新しいリクエストが入力されると、そのリクエストのプレフィルを1ステップで処理する必要がありますが、プロンプトが長いとそのステップに時間がかかり、 その間バッチ内の他のリクエストは次のトークンを受け取れません。 Sarathi-Serveの論文(Agrawalら、OSDI 2024)ではこれをgeneration stallと呼び、Yi-34B・A100 2枚の条件においてこのような停止が数秒に及ぶ可能性があると報告しています。
解決策は図(ダ)のように 長いプレフィルをチャンクに分割して複数のステップに分散させ、ステップごとにデコードトークンと混在させることです(チャンクプリフィル)。同論文の表4(Yi-34B、A100 2枚、1ステップのトークン予算1,024)によると、P99のトークン間隔は混在させるだけの場合は0.68秒、チャンクと「デコードの優先充填」を併用した場合は0.14秒でした。テールレイテンシの制約下で処理できるワークロードは、vLLMと比較してMistral-7B(A100 1枚)で2.6倍、Yi-34B(A100 2枚)で最大3.7倍でした(論文の抽象)。
独自の実測。 8GBクラスのGPUにおいてQwen3-1.7Bで300トークンをデコードする1つのリクエストが実行されている間に、3,500トークンの入力3つを挿入し、1ステップあたりに投入する入力トークン数(--max-num-batched-tokens)を変更した区間のトークン間隔を測定しました。
| 1ステップの入力トークン上限 | 挿入区間の最大トークン間隔 | 長い入力自体の最初のトークン |
|---|---|---|
| 512 | 69ms | 481〜494ms |
| 1,024 | 129ms | 468〜493ms |
| 2,048(本バージョンのデフォルト) | 227ms(3回とも227・226・226) | 466〜480ms |
| 8,192(実効3,500) | 445ms | 461〜474ms |
- 最大トークン間隔はチャンクサイズにほぼ比例しました。このGPUの入力処理速度(約8,200トークン/秒)で算出した1つのチャンクの時間(512トークンで約62ms、2,048トークンで約250ms)と一致しています。
- チャンクを小さくすると、長い入力の最初のトークンが3〜5%遅くなる代わりにデコードの途切れが少なくなりました。対話や音声のようにトークンが均等に出力されるべきサービスでは小さなチャンク、長いドキュメントをまとめて処理するタスクでは大きなチャンクが適しています。
チャンクプリフィルは間隔を 短縮する ことはできますが、完全になくすことはできません。プレフィルとデコードが同じGPUを共有している限り、互いに干渉し合います。第4回の分離サービングはこの問題を別の方法で解決します。
4. プレフィックスキャッシュ:同じ前半部分は1度だけ計算する
リクエスト間でも同じ前半部分が多く存在します。すべての対話に付与されるシステムプロンプト、同じドキュメントに対して複数回質問するRAG、前のターンをそのまま再度送信するマルチターン対話などがそれに該当します。第1回で確認したように、前半部分が同一であればそのKVも同一です。そのため、 すでにGPU上に存在するブロックを検索してそのまま使用すれば、その分のプレフィルをスキップできます。
vLLMの自動プレフィックスキャッシュ(設計ドキュメント、2026-10-13閲覧)はブロックごとにキーを生成します。
- キー = ハッシュ(前ブロックのキー、このブロックのトークンID群、追加情報)
- 追加情報には、LoRAアダプター、マルチモーダル入力のハッシュ、ユーザー間を分離するcache saltが含まれます。
- 前ブロックのキーが含まれるため、1つのキーが 最初からそのブロックまでの前半部分全体を代表します。同じ16個のトークンであっても、前半部分が異なればキーも異なります。
- 埋まったブロックのみ キャッシュされます。
- リクエストが終了してもブロックは即座に削除されず、「空だが内容が残っている」キューに配置され、容量が必要になった場合は最も長く使用されていないもの(LRU)から削除されます。終了したリクエストのブロックは 後方から キューに投入され、再利用される可能性が低い末尾側が優先して削除されるようにします。
- ハッシュ関数のデフォルトはv0.11以降sha256です。
独自の実測。 3,000トークンの共通プレフィックスに対し、リクエストごとに異なる32トークンの末尾を付与して順次送信しました。
| 設定 | 初回リクエストの最初のトークン | 以降のリクエストの最初のトークン(中央値) |
|---|---|---|
| プレフィックスキャッシュ有効(デフォルト) | 394ms | 28ms |
| 無効 | 395ms | 393ms |
ヒットすると3,000トークンのプリフィルをスキップでき、最初のトークンが14倍速くなりました。逆に前半が全く一致しない負荷(入力2,048、同時32)で有効にした場合のコストは、スループット約1.2%、初SLA中央値約8%でした(ハッシュ計算コストと推測され、分解はしていません)。
測定時の落とし穴。 同じ文を繰り返して性能を測定すると、プレフィックスキャッシュがヒットしてGPUが出せないような速度が出てしまいます。性能テストの入力はリクエストごとに変えるべきであり、キャッシュの効果を確認したい時だけ共通の前半部分を意図的に作成します。
5. KVの容量が足りない場合
バッチサイズの上限がKVであることは数値からもわかります。同じ実測環境でKVの容量は22,048トークンであり、入力512 + 出力128 = 640トークンのリクエストであれば約34個が入ります。
| 同時リクエスト | スループット | 初トークン中央値 |
|---|---|---|
| 1 | 66トークン/秒 | 73ms |
| 8 | 383トークン/秒 | 351ms |
| 32 | 831トークン/秒 | 666ms |
| 64 | 787トークン/秒 | 5.7秒 |
32から64に増やすとスループットは逆に減少し、初トークンが8.6倍遅くなりました。容量のないリクエストはキューで待機します。さらに不足すると すでに実行中だったリクエストがKVを手放して追い出され(プリエンプション)、最初から再びプリフィルされます。 入力2,048・同時32でプリエンプションが7回、入力1,500・出力256・同時64で22回発生し、初トークンの中央値はそれぞれ12.5秒、35.4秒でした。同じKVを2回計算することになるため、スループットは向上せず遅延のみが増加します。
KVの容量はサーバーを起動するたびに22,048〜27,504トークンと異なり、同時64のような限界付近ではその20%の差が初トークンを2.7倍変化させました。運用においてはサーバーログの GPU KV cache size を記録し、同時実行数の上限を「平均リクエスト長でKVが保持するリクエスト数」付近に合わせるのが望ましいです。
6. まとめ:そしてGPUメモリの壁
| 手法 | 根拠 | 確認された効果 |
|---|---|---|
| PagedAttention | 論文 | 無駄を1つのブロック内に抑え、同等の遅延でスループット2〜4倍 |
| 連続バッチング | 論文(Orca) | 静的バッチと比較して同等の遅延で36.9倍(175Bクラス、他設計含む) |
| チャンクプリフィル | 論文(Sarathi-Serve)・実測 | P99トークン間隔 0.68→0.14秒(論文)、最大間隔 445→69ms(実測) |
| プレフィックスキャッシュ | 実測 | 3,000トークンのプレフィックスで初トークン 394→28ms |
これらの手法はすべて GPUメモリ内 で動作します。プレフィックスキャッシュはGPUの容量が足りなくなると古いブロックから削除し、削除された前半部分が再び来ると最初から計算します。エンジンのインスタンスが複数あるとそれぞれ個別にキャッシュするため、隣のインスタンスですでに計算されたKVを利用できません。第3部では、KVをGPU外(CPUメモリ、ディスク、他のサーバー)に配置するLMCacheを取り上げます。
Titiの一言。 卓球場のテーブルを「1チームが一日中」予約すると、空きテーブルが多くなります。30分単位で分割し、終わった場所にすぐ次のチームを入れ、同じサークルで1つのテーブルを共有できるようにすれば、同じ体育館ではるかに多くの人がプレーできるようになりますよね。PagedAttentionと連続バッチングはその役割を果たします。
参考
- Kwonほか, Efficient Memory Management for Large Language Model Serving with PagedAttention (SOSP 2023): https://arxiv.org/abs/2309.06180 (第1・3.1・4.3・6.2・6.3・7.2節、表1)
- Yuほか, Orca: A Distributed Serving System for Transformer-Based Generative Models (OSDI 2022): https://www.usenix.org/conference/osdi22/presentation/yu (第6.2節、図10b)
- Agrawalほか, Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve (OSDI 2024): https://arxiv.org/abs/2403.02310 (アブストラクト、第3.2・4節、表4)
- vLLM自動プレフィックスキャッシュ設計: https://docs.vllm.ai/en/latest/design/prefix_caching.html (開発版ドキュメント、2026-10-13閲覧)
- 実測の引用:同サイト「トス ML/AI Infra 準備」第14編(8GBクラスGPU、Qwen3-1.7B、vLLM 0.31.0)