TT Lab
ブログ

한국어 日本語

はじめる
ブログ

ブログ

サービングエンジン内のKVキャッシュ:PagedAttention、連続バッチ処理、プレフィックスキャッシュ

状態:ドラフト(2026-10-13)、一部実測。 論文と公式ドキュメントは2026-10-13に直接アクセスして確認しました。実測値は同サイトの「トス ML/AI Infra 準備」第14回において、8GBクラスのGPU1枚、Qwen3-1.7B、vLLM 0.31.0の環境で測定された値を再引用しています。大規模モデルや複数GPUにおける値は未測定です。 作成ルール:実測データを基本とし、測定できなかったものは「未測定」、確認できなかったものは「未確認」と表示します。

目次

1. スループットは結局バッチサイズの問題である

第1回の結論を再掲します。デコードはトークンを1つ生成するたびに重み全体をメモリから読み込む必要があるため、リクエストを1つだけ処理する場合はGPUの演算器のほとんどが遊んでしまいます。重みを1回読み込む際に複数のリクエストを同時にデコードすれば、同じ読み込みで複数のトークンを生成できます。そのため、 バッチを大きくするとスループットが向上します。

バッチに含まれるリクエストは、それぞれ自身のKVをGPUメモリ上に保持する必要があります。重みを配置した残りのメモリをKVで分割して使用するため、 同じメモリにKVをどれだけ高密度に格納できるかがバッチサイズ、すなわちスループットを決定します。 サービングエンジンのKV管理手法のほとんどは、この1文のために存在しています。

手法 節約する対象 スループットに到達するアプローチ
PagedAttention 予約や断片化によって無駄になるKVメモリ 同じメモリにより多くのリクエスト → より大きなバッチ
連続バッチ処理 早期終了したリクエストの空きスロット ステップごとにバッチをフルに満たす
チャンクプリフィル 長いプレフィルがデコードを停止させる時間 トークン間隔の制約を守りながらバッチを拡大する
プレフィックスキャッシュ 同じ前半部分のKV計算とメモリ プレフィルの計算をスキップしてブロックを共有する

2. PagedAttention:KVをブロックに分割する

KV 메모리를 나누는 두 방법 옛 방식 — 요청마다 최대 길이만큼 이어 붙여 예약 A B A 예약 B 예약 틈 틈 아직 안 쓴 예약과 사이의 틈은 다른 요청이 못 쓴다. vLLM 논문: 기존 시스템은 KV 메모리의 20.4~38.2% 만 실제 토큰에 썼다. PagedAttention — 16토큰 블록과 블록 표 요청 A 요청 B A0A1B0 A2 B1 블록 표 A → 3, 7, 1 B → 3, 5 GPU 메모리 0 1 2 3 공유 4 5 6 7 8 9 10 11 · 블록은 빈 곳 어디에나 놓인다 → 틈이 생기지 않는다. · 낭비는 요청마다 마지막 블록의 빈칸뿐이다. · A0 과 B0 은 같은 프롬프트라 3번 블록 하나를 함께 본다. 블록 열쇠 = (앞 블록 해시, 이 블록의 토큰, LoRA 등 추가 정보) 토큰이 찬 칸 예약만 한 칸 빈 블록
図:連続予約とブロック分割の比較

旧方式の無駄

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つ割り当てられます。

アテンションカーネルはブロックテーブルを辿りながら分散したブロックから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. 連続バッチ処理:空いたスロットをステップごとに埋める

GPU 한 걸음마다 배치에 무엇이 들었나 가로 = 시간, 줄 = 배치의 자리, 칸 하나 = 한 걸음 (가) 정적 배치 — 모두 끝나야 다음 배치 자리 1자리 2자리 3 빈 자리, 새 요청 대기 (나) 연속 배칭 — 빈 자리에 바로 새 요청 자리 1자리 2자리 3 긴 걸음 자리 1·3 의 토큰이 늦다 (다) 청크 프리필 — 긴 프리필을 잘라 디코드와 섞기 자리 1자리 2자리 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を共有している限り、互いに干渉し合います。第4回の分離サービングはこの問題を別の方法で解決します。

4. プレフィックスキャッシュ:同じ前半部分は1度だけ計算する

リクエスト間でも同じ前半部分が多く存在します。すべての対話に付与されるシステムプロンプト、同じドキュメントに対して複数回質問するRAG、前のターンをそのまま再度送信するマルチターン対話などがそれに該当します。第1回で確認したように、前半部分が同一であればそのKVも同一です。そのため、 すでにGPU上に存在するブロックを検索してそのまま使用すれば、その分のプレフィルをスキップできます。

vLLMの自動プレフィックスキャッシュ(設計ドキュメント、2026-10-13閲覧)はブロックごとにキーを生成します。

独自の実測。 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と連続バッチングはその役割を果たします。

参考

ログインするといいねできます

コメント

まだコメントはありません。

ログインするとコメントできます