11月5日、ngrokが「Serving models at scale with vLLM」と題した記事を公開した。この記事では、オープンウェイトモデルの大規模推論サーバーとして広く使われるvLLMの内部構造と、その中核技術であるPagedAttentionや連続バッチングの仕組みについて詳しく紹介されている。従来の推論フレームワークではKVキャッシュのメモリ予約が無駄を生み、同時処理できるリクエスト数を大きく制限していた。vLLMはこの問題をOSのページング機構から着想したブロック管理で解決し、GPUの稼働率とメモリ効率を同時に引き上げる設計を採用している。
LLM推論スケールの根本問題
LLMの推論処理は、大きく2フェーズに分かれる。
- Prefill(プリフィル):プロンプト全体を一度に処理し、最初のトークンを生成する
- Decode(デコード):それ以降、1トークンずつ生成するフェーズ
問題はDecodeにある。トークンを1つ生成するたびに、GPUはモデルの全重みをメモリから引き出して演算を行う。GPUの並列処理能力を考えると、これは著しく非効率だ。しかも、その間ほかのリクエストは待ちぼうけになる。
連続バッチングで処理効率を上げる
複数リクエストをまとめて処理する「バッチング」は効率化の基本だ。ただ、単純な静的バッチング(static batching)では、あるリクエストが終わるまで次のリクエストを投入できない。先に終わったリクエストの「席」が空いたまま無駄になる。
そこで登場するのが連続バッチング(continuous batching)だ。リクエストが完了した瞬間、待機中のリクエストのトークンを次のフォワードパスに加える。GPUの稼働率を最大限に保つ仕組みで、現在のプロダクション推論サーバーの主流となっている。
ただし、連続バッチングを機能させるには別の問題を解決する必要がある。KVキャッシュのメモリ管理だ。
KVキャッシュとメモリの浪費
LLMがトークンを生成する際、直前までの全トークンのコンテキストが必要になる。毎回再計算するのは非現実的なため、各トークンの計算結果(KeyとValue)をGPUメモリにキャッシュする。これがKVキャッシュだ。
問題は、レスポンスが何トークンになるかが事前にわからないことにある。従来の推論フレームワークは安全策として、各リクエストに「最長レスポンス分」のメモリをあらかじめ確保する。結果、大半のメモリが空のまま占有され、連続バッチングで詰め込めるリクエスト数が減る。元記事によれば、この内部フラグメンテーションと過剰予約によって、実際のGPUメモリ利用効率は理論値を大きく下回るのが従来フレームワークの共通課題だった。
vLLMの核心:PagedAttention
vLLMはUC BerkeleyのSky Computing Labで開発され、2023年の論文「Efficient Memory Management for Large Language Model Serving with PagedAttention」でPagedAttentionを発表した後、大規模なオープンソースコミュニティによってメンテナンスされている推論サーバー兼フレームワークだ。
vLLMの最大の特徴がPagedAttentionである。OSのメモリ管理における「ページング」から着想を得た手法で、KVキャッシュを固定サイズのブロック(通常16トークン分)に分割して管理する。
- リクエストは起動時に大きなメモリを予約しない
- KVデータがブロックを埋めたタイミングで、次のブロックを確保する
- ブロックはGPUメモリ上の任意の場所に散在してよい
- ブロックテーブルがどのブロックにどのリクエストのデータがあるかを管理する
これにより、各リクエストが実際に使う分だけのメモリしか消費しない。無駄な予約がなくなり、連続バッチングに使える「席」が大幅に増える。
プレフィックスキャッシング
さらに、プレフィックスキャッシング(prefix caching)も備わっている。同じシステムプロンプト(例:You are a helpful customer service agent.)で始まるリクエストが複数あるとき、その共通部分のKVデータを再計算せずに共有する仕組みだ。
ポイントは「同じ単語」ではなく「同じ位置の同じトークン列」であることが条件になる点だ。途中で1トークンでも異なれば、それ以降は共有できない。vLLMは各ブロックにハッシュ値(前のブロックのハッシュ+内部のトークンから計算)を付与し、ブロック単位でプレフィックスの一致を判定する。
チャットボットでの効果が特に大きい。メッセージを送るたびに会話履歴全体がモデルに送られる仕様上、プレフィックスキャッシュがなければ毎回すべてを再計算することになる。長い会話履歴が蓄積するほど計算コストが増大するため、このキャッシュの恩恵は会話ターン数に比例して大きくなる。
スケジューラの役割
フォワードパスのたびに、スケジューラがどのリクエストのトークンをバッチに含めるかを決定する。トークン予算内で、実行中のリクエスト(デコード中)を優先し、余裕があればプリフィル待ちのリクエストを追加する。
KVブロックが枯渇した場合は「プリエンプション」が発生する。最も新しく追加されたリクエストのKVブロックを解放し、待機列の先頭に戻す。解放されたブロックをほかのリクエストに割り当て、プリエンプトされたリクエストは後でプロンプトから再計算する。
また、プロンプトが非常に長い場合(例:50,000トークン)はチャンクドプリフィル(chunked prefill)が働く。巨大なプロンプトを一度に処理するのではなく、トークン予算に収まるチャンクに分割して複数のフォワードパスにまたがって処理する。これにより、長いプロンプトの処理中にほかのリクエストが完全に止まることを防ぐ。
vLLMはPagedAttention・連続バッチング・プレフィックスキャッシング・チャンクドプリフィルといった複数の技術を組み合わせ、GPUの稼働率とメモリ効率を両立させている。同様の推論サーバーとしてはHugging FaceのText Generation Inference(TGI)やNVIDIAのTensorRT-LLMなども存在するが、vLLMはOpenAI互換APIを標準提供しつつPagedAttentionを中核に据えた設計の透明性と、活発なコミュニティ規模を強みとしている。大規模LLM推論の実装を検討する際、まず理解すべきアーキテクチャといえる。
詳細はServing models at scale with vLLMを参照していただきたい。