BLOG
vLLMのPagedAttention:VRAM管理が大規模モデル推論をどう支えているか
技術分解:AI技術フレームワークの解説——説明、分析、技術評価、価値判断、実運用。 著者:永亮
2023年、大規模モデル推論には共通の悩みがあった:GPUのVRAMは十分に見えるが、実際に動かしてみるとバッチサイズを大きくできない。原因はモデルの重みにあるわけではない——重みは静的で、一度計算すればどれだけの容量を占めるか分かる——問題はKVキャッシュにある。会話が長くなるにつれて絶えず膨張し、かつリクエストごとにその長さが異なる。UC BerkeleyのSky Computing Labは同年、後に何度も引用されることになる答えを出した:オペレーティングシステムの仮想メモリページングを注意機械計算に持ち込み、PagedAttention、そしてその背後にあるサービスエンジンvLLMが誕生した。
それから2年以上が経ち、vLLMはSOSP論文から推論サービスの事実上の標準へと成長した:2026年9月時点で、GitHub約9.2万スター、PyPI週間ダウンロード約41.5万回、貢献者は2000人を超える。本記事でも変わらず6つのことを分解する:それが何か、中核のメカニズムからソースコードレベルまで、データによる評価、SGLangとの選定、使う価値があるか、どう導入するか、そして——もし自分で同様のVRAM管理を書きたい場合、最小限の考え方はどうなるか。
一、これは何か
一言で位置づける:vLLMは高スループットかつメモリ効率の高いLLM推論・サービスエンジン(GitHubリポジトリの説明文の通り)であり、解決すべき中核的な問題は——KVキャッシュが推論VRAMの大半を占めており、管理が雑だと無駄が生じ、無駄はバッチサイズを圧迫し、バッチサイズはスループットを直接決定する——ということだ。
重要な事実:リポジトリは2023年2月に作成され、当初はUC BerkeleyのSky Computing Lab由来。論文『Efficient Memory Management for Large Language Model Serving with PagedAttention』はシステム分野のトップ会議SOSP 2023で発表され、arXiv番号は2309.06180。9人の著者のうちにはIon StoicaやJoseph Gonzalezといった分散システム分野の大物が含まれる。ライセンスはApache-2.0。約91,598スター、22,113フォーク。READMEには「数十の学術機関と企業からなるコミュニティによって維持されている」とあり、貢献者リストはコミット数順で、トップ100開発者の合計コミット数は1.2万回を超える——スター数は操作で増やしたものではなく、チームが長期的に投入している証拠だ。
二、中核のメカニズム
2.1 まず計算をクリアに:KVキャッシュは実際にどれくらいVRAMを食うか
PagedAttentionは突拍子もない思いつきではなく、まず計算をクリアにしたところから生まれた。この計算は今日でも推論に携わる全員が覚えておく価値がある:
1トークンあたりのKVキャッシュバイト数 = 2 × 層数 × KVヘッド数 × ヘッド次元 × パラメータあたりバイト数(2倍するのはKとVがそれぞれ1つずつあるため)。
Qwen2.5-72Bの公式設定(Hugging Face config.json)を当てはめる:80層、GQAアーキテクチャで8つのKVヘッド、ヘッド次元128、fp16精度で2バイト——1トークンあたり327,680バイト、約0.31MBを占める。8kコンテキストのリクエストなら、KVキャッシュだけで2.5GB必要;100並列なら250GBだ。ここでGQAが大いに貢献している:64の注意ヘッドが8つのKVヘッドを共有しており、もし古いマルチヘッド注意アーキテクチャだったら、この数字はさらに8倍になる。
この計算は、論文にある3つの種類の無駄がなぜ致命的かを説明している。予約:古いシステムは推定される最大長に基づいて、各リクエストに対して一度に連続したVRAMを確保し、多くの領域が空いたままになる。内部フラグメンテーション:実際の長さが推定長に達せず、末尾が無駄になる。外部フラグメンテーション:VRAMが大小さまざまな空洞に切り刻まれ、連続した大きな塊を確保できない——古いシステムはVRAMの連続性を要求するが、これは自ら掘った穴だ。論文の実験結論は:古いシステムではKVキャッシュの有効利用率は約20%まで低下し、8割のVRAMが無駄に占められる。サービスはVRAM制限のあるシナリオであり、無駄に占められる直接的な結果はバッチサイズを大きくできず、スループットが上がらないことだ。
2.2 PagedAttention:OSページング思想の3つの要素
PagedAttentionのアイデアは驚くほど直截だ:OSは仮想メモリを管理する際、プロセスが見る連続したアドレス空間を固定サイズのページに切り、ページテーブルを使って物理メモリにマッピングする——KVキャッシュも同じように管理してはどうだろう?
実装レベルでは3点セットだ。第一に、ページング:KVキャッシュはリクエストごとに連続した空間を確保するのではなく、固定サイズのブロック(論文の実験では16トークンを1ブロックとすることが多い)に分割し、必要な時にだけブロックを確保する。第二に、ブロックテーブル(block table):各リクエストは論理ブロック番号から物理ブロック番号へのマッピングテーブルを維持し、注意カーネルはこのテーブルに従って対応する物理ブロックをgatherする。物理ブロックは連続している必要がない。このステップが本文全体の鍵だ——「連続」という仮定こそが外部フラグメンテーションの元凶であり、ブロックテーブルはそれを完全に排除した。第三に、オンデマンド割り当て:ブロックは左から右へ埋まってから新しいブロックを要求し、各リクエストの無駄は1ブロック以内に圧縮され、ゼロに近い無駄(論文の基準)となる。
2.3 Copy-on-write:共有ブロックの参照カウント
ページングは計画外の利点ももたらす:共有だ。並列サンプリング(同じプロンプトからn個の候補を生成)、ビームサーチの複数パスでは、プレフィックスのKVは完全に同一——古いシステムはそれぞれ別々に保存するか、特殊なロジックで無理やり共有するしかなかった。vLLMのアプローチはOSと全く同じ:共有ブロックの参照カウントを増やし、誰かが書き込もうとして参照カウントが1より大きい場合は、コピーしてから書く(copy-on-write)。結果としてプレフィックスKVはリクエスト内およびリクエスト間で再利用でき、論文はこれをPagedAttentionの2番目の大きな貢献として挙げている:ゼロに近い無駄に加え、柔軟な共有も実現した。
2.4 ソースコードレベル:V1のブロックプールとハッシュチェーン
2025年、vLLMはV1エンジンの書き直しを完了し、現在すべてのスケジューリングロジックはvllm/v1ディレクトリ配下にあり、旧エンジンのコードは削除された。ブロック管理のエントリポイントはvllm/v1/core/block_pool.pyのBlockPoolで、2つの構造を管理している:
1つ目はフリーブロックキュー free_block_queue——双方向連結リストで、「最近使用された」順にソートされている。再利用されたブロックは解放されるとキューの末尾に戻り、新規割り当てはキューの先頭から取得する。先頭のブロックがまだプレフィックスキャッシュの内容を持っている場合は、キャッシュから追い出してから割り当てる。LRU追い出しには追加のデータ構造は不要で、キューの順序自体が追い出し順序となる。2つ目はハッシュからブロックへのマッピング cached_block_hash_to_blockで、プレフィックスキャッシュの検索を支える。
ブロックのハッシュは単なる「ブロック内容のハッシュ」ではなく、チェーン式になっている。kv_cache_utils.pyのhash_block_tokensは次のようになっている:sha256(親ブロックハッシュ、自ブロックのトークンシーケンス、追加キー)。親ブロックハッシュがソルトとなる——プレフィックス内容が1トークンでも違えば、後続のチェーン全体のハッシュがすべて異なり、異なるプレフィックスが同じキーに衝突することはない。関数自体はLRUキャッシュを持っており、同じブロック内容を再計算する必要はない。新規リクエストが入ってくると、kv_cache_manager.pyのget_computed_blocksが自身のプロンプトを使ってこのハッシュチェーンをブロックごとに比較し、ヒットしたブロックは参照カウントを増やして直接引き継ぎ、対応する部分のprefillを完全にスキップする。
ソースコードには言及すべき詳細がある:プロンプトのすべてのブロックがキャッシュにヒットしても、最後のトークンは再計算しなければならない——logitsを取得するには現状の計算が必要だからだ(ソースコードのコメント原文:When all tokens hit the cache, we must recompute the last token to obtain logits)。この「全ヒットでも1ステップ計算する」という小さな設計により、キャッシュヒットが出力の意味を変えないことが保証されている。
2.5 スケジューラ:prefill、decode、プリエンプションを1つのループで処理
スケジューリングのエントリポイントはvllm/v1/core/sched/scheduler.pyにある。このファイルの先頭には著者Woosukによるコメントがあり、全文読む価値がある:スケジューラには**「decodeフェーズ」と「prefillフェーズ」の区別がなく**、各リクエストは2つの数——num_computed_tokens(計算済みトークン数)とnum_tokens_with_spec(計算すべき位置で、プロンプト長+生成長+投機トークンに等しい)——だけを維持している。スケジューラが各ステップで行うことは、各リクエストにトークンクォータを割り当て、前者を後者に追いつかせることだ。
この抽象化の利点は、3つのシナリオが同じループで統一的に処理されることだ:長いプロンプトをチャンクに分けてバッチに混ぜる(chunked prefill、1つの長いリクエストがdecodeの群を飢えさせない);プレフィックスキャッシュにヒットしたリクエストは中間の位置から直接続ける;投機的デコードで候補トークンをいくつか余計に計算するのも、単にnum_tokens_with_specを少し増やすだけだ。各ステップの総クォータはmax_num_scheduled_tokensで制限される。
VRAMが不足した時の手段はプリエンプションだ:_preempt_requestはrunningキューの末尾にあるリクエストをwaitingキューに戻し、その全ブロックとエンコーダキャッシュを解放する。V1のプリエンプションには再計算(recompute)しかない——戻されたリクエストは次に順番が回ってきた時に最初からprefillをやり直し、CPUメモリへのスワップアウト(swap)は行わない。これは明らかなトレードオフだ:再計算は演算能力を無駄にするが、実装はシンプルで、CPUとGPU間のKVキャッシュの転送によるスラッシングも避けられる。実際の本番システムでは、プリエンプション回数は監視すべき指標だ——頻繁なプリエンプションは容量設定やスケジューリングパラメータに問題があることを示している。
V1ではさらに2層のオーバーラップを行っている:出力処理(detokenize、ストリーミング送信)とGPU計算のオーバーラップ;非同期スケジューリングモードでは、次のステップのスケジューリング決定と現在のステップの計算のオーバーラップだ。スケジューラは純粋なPythonであり、この種のオーバーラップはタダで手に入る最適化だ。
2.6 ブロックテーブルからGPUへ:カーネル、バックエンド、マルチプロセス
ブロックテーブルは最終的にGPU上のデータになる:各ステップのスケジューリングでblock_tableをテンソル形式で注意カーネルに渡す。decodeではvLLMがcsrc/attention配下でCUDA/CUTLASSを使って手書きしたpaged attentionカーネルを使用し、テーブルに従って物理ブロックをgatherする。prefillの注意形状は異なる(プロンプト全体を一度に計算するため)、このパスを通らず、FlashAttentionやFlashInferなどの既存のバックエンドに直接接続する。その上にはAttentionBackend抽象レイヤーがあり、NVIDIA、AMD、CPU、TPUがそれぞれ独自のバックエンドを実装し、カーネルの詳細はスケジューラから見えない。
decodeステップの形状は高度に整っている(各リクエストの各ステップでちょうど1トークン増えるだけ)、V1はdecodeステップ全体をCUDA Graphでキャプチャし、PythonとCUDA間の起動オーバーヘッドを排除する。prefillとdecodeが混在するシナリオでは、ピースワイズコンパイル(piecewise compilation)によって形状が変化する部分をグラフの外に残す。このステップはページングと相補的だ:各リクエストがブロック単位でVRAMを増減するため、バッチ内で誰が実行し誰が脱落しても他のリクエストのVRAMレイアウトに影響せず、ステップ計算全体が安定してグラフとしてキャプチャできるのだ。
最後にマルチプロセスの骨格だ:APIサーバープロセスはHTTPリクエストを受け、トークン化とマルチモーダルロードを行い、ZMQを介してengine coreと通信する(複数のAPIサーバー対複数のengine coreのmany-to-manyトポロジー)。engine coreプロセスはスケジューリング決定を行う。GPUワーカープロセスはカードに1つ、数はDP×PP×TPに等しく、順方向計算のみを担当する。データ並列を有効にすると、負荷分散のためのDPコーディネータも存在する。シングルノード4カードテンソル並列の標準的なデプロイメントは、1つのAPIサーバー + 1つのengine core + 4つのワーカー、計6プロセスとなる。
三、技術評価:2-4倍は論文の基準、比較相手を見る
論文のデータ:同じレイテンシ水準で、vLLMのスループットは当時最強のシステムであるFasterTransformerやOrcaより2-4倍高い。シーケンスが長くなるほど、モデルが大きくなるほど、デコードアルゴリズムが複雑になるほど、優位性は顕著になる(論文原文)。ベースラインに注意——これは2023年の比較であり、Orcaはすでに当時最も進んだイテレーションレベルのスケジューリングシステムだった。vLLMが勝ったのはVRAM管理によってであり、演算子によるものではない。
私の解釈は2層だ。第一層、論文にある「旧システムの有効VRAM利用率は約20%まで低下する」という測定は、2-4倍という数字よりも本質的だ——なぜ改善が成立するかを説明している:無駄を8割から1ブロック以内に圧縮すれば、同じカードで数倍のリクエストを詰め込め、スループットは自然と倍増する。この因果関係は確固たるものだ。第二層、2026年の座標に置くと、2-4倍はもう宣伝文句として使えない。今日の競合相手はSGLang、TensorRT-LLMのクラスであり、ギャップはワークロード依存の数割%に縮まっている。
人気は3つの数字で見る:スター約9.2万;貢献者トップ100で合計コミット1.2万回超;PyPI週間ダウンロード約41.5万回。この3つの数字は相互に整合しており、実際の使用規模は疑いようがない——推論サービス分野のナンバーワンインフラだ。
冷や水も浴びせておく。ベンチマークは論文の基準であり、自社での再現は自分のリクエスト長分布と並列パターン次第だ。プレフィックスキャッシュは短いリクエストや多様なプロンプトのシナリオでは利益が限られ、VRAMを無駄に占めるだけになる。V1のマルチプロセスアーキテクチャはシングルカードの小さなモデルではCPUオーバーヘッドがあり、公式ドキュメントもGPU数に応じてCPUリソースを設定することを推奨している。
四、SGLangと比較してどう選ぶか
SGLangのプロフィールを一言で:LMSYS出身の高性能サービスフレームワーク。2024年1月作成、約3.6万スター、Apache-2.0。中核的な差別化はRadixAttention——プレフィックスキャッシュをハッシュテーブルではなく基数木(Radix Tree)で構成すること。公式基準では「最大5倍の推論高速化」(2024年1月公式ブログ、自評データ)。ここ1年、エージェントワークフローやRLトレーニングフレームワーク(verl、slimeなど)での採用率が高い。
選定は3つの言葉で:明確な理由がなければデフォルトでvLLM——エコシステム、ドキュメント、ハードウェア対応(NVIDIA/AMD/Intel/CPUは第一級サポート、TPU/Gaudi/Ascend/Apple Siliconはプラグイン)が最も充実しており、トラブルシューティングの検索コストが最も低い。負荷がマルチターンエージェント呼び出し、複雑な構造化出力、RLトレーニングループへの組み込みである場合は、SGLangを同じ土俵で負荷テストに引き入れる価値がある。これらのシナリオではSGLangの激しい最適化に実測での優位性がある。両者ともApache-2.0であり、移行コストは高くない。自分のトラフィックに語らせると良い。
理念上、両者は同源だ:RadixAttentionとPagedAttentionのプレフィックス共有は同じ発想の異なるデータ構造(基数木対ブロックテーブルハッシュ)であり、SGLangプロジェクト自体もvLLMのインフラを大量に流用している。これはゼロサム競争ではなく、業界全体が「KVキャッシュは再利用可能なリソース」というコンセンサスに収束しているのだ。
五、価値判断
真の問題:推論コストはVRAM効率×バッチサイズに等しい。vLLMはKVキャッシュを「予約制」から「ページング制」に変え、私の見では2023年以来の推論スタックにおいて最も重要な単独の改善だ——モデルもチップも変えず、システムソフトウェアだけで同じハードウェアから数倍のスループットを絞り出した。今日、ほぼすべてのオープンソース推論フレームワークがそのブロック管理思想を使っており、このこと自体が価値の証明だ。
境界も明確だ。シングルノードで小さなモデルを動かし、並列数が一桁の場合、ページングと連続バッチ処理による利益は限られ、シンプルなソリューションの方が気が楽かもしれない。最初のトークンレイテンシ(TTFT)は解決しない——prefillの演算ボトルネックはカーネルと並列戦略に依存し、それは別の戦場だ。非標準アーキテクチャ(Mambaのような状態空間モデル)のサポートはV1でHybridKVCacheCoordinatorという層ができたばかりで、まだ進化中。いつ使うべきか:真剣にLLMサービスを外部提供するあらゆるシナリオで、それがデフォルトの起点だ。いつ使うべきでないか:オフラインで数回データを走らせるだけ、あるいはトレーニングで使う——それは別のツールの領分だ。
六、どう導入するか
インストールは一行:
uv pip install vllm # または pip install vllm
対外的なサービスの起動も一行で、vllm serveがOpenAI互換インターフェースを起動する(同時にAnthropic Messages APIとgRPCをサポート):
vllm serve Qwen/Qwen3-32B --tensor-parallel-size 2
オフラインのバッチ推論にはPythonのLLMクラスを使い、数行で結果が得られる。200以上のモデルアーキテクチャをサポート——decoder-only、MoE、混合注意/状態空間、マルチモーダル、embedding、報酬モデルすべてをserving可能で、並列サンプリング、ビームサーチ、構造化出力(xgrammar/guidance)、ツール呼び出し解析、マルチLoRAもカバーする。分散側にはテンソル、パイプライン、データ、エキスパート、コンテキストの5種類の並列が設定可能。導入アドバイスは2つ:prefix cachingはトラフィック特性に応じてオン・オフを決めること(マルチターン対話系負荷ではオン、短リクエスト高多様性負荷ではオフ);本番環境ではKVキャッシュ利用率とプリエンプション回数を監視に含めること。この2つの指標はGPU利用率よりも早く容量問題を暴露する。
七、自分で同様のソリューションを作るには
「自分で一式書く」は空想の題目ではない。GitHub上のnano-vllm(GeeeekExplorer/nano-vllm、2025年6月オープンソース、MIT、約1.5万スター)は、コミュニティがvLLMのソースコードを読んで書いた最小限の読みやすいレプリカで、エンジン全体が千行規模だ——これは1つのことを証明している:PagedAttentionの中核思想は1枚の紙で説明できる。骨格は6ステップ:
- ブロックプールとブロックテーブル:起動時にKVキャッシュVRAMを固定サイズの物理ブロックプールに切り分ける。各リクエストは論理ブロックから物理ブロックへのテーブルを維持し、注意はテーブルに従ってgatherする。
# 最小の骨格:ブロックプール
free_blocks = deque(range(num_gpu_blocks)) # 物理ブロックの空きキュー
block_table = {} # req_id -> [物理ブロック番号]
-
オンデマンド割り当て:各ステップのdecode後、現在のブロックがいっぱいになったか確認し、いっぱいになって初めて空きキューからブロックを1つ取得して繋ぐ——無駄は自然と1ブロック以内になる。
-
参照カウントとコピーオンライト:共有ブロックのrefcountを増やす。書き込み前にrefcountが1より大きければ、コピーしてから書く。並列サンプリングとビームサーチの共有はこれだけで成り立つ。
-
スケジューリングループは2つの数だけを追う:各リクエストはnum_computedとnum_totalを記録する。各ステップでトークン予算内でリクエストを選び、computedをtotalに追いつかせる。話が終わったら退出し、収まらなければ優先度でプリエンプションする(リクエストを差し戻し、ブロックを解放し、後で最初からprefillを再計算する)。chunked prefill、プレフィックスキャッシュ、投機的デコードはすべてこのループの特例に過ぎない。
-
プレフィックスハッシュキャッシュ:sha256(親ブロックハッシュ + 自ブロックトークン)でチェインハッシュを構築し、ブロックごとに比較してヒットすればprefillをスキップする。空きキューの先頭が追い出し候補だ。
-
カーネルは自分で書かない:注意はFlashAttentionに直接繋ぎ、CUDA/CUTLASSは本当に性能チームがいる時に残せばいい。nano-vllまではそうしている。
この6ステップを合わせれば、数百行のPythonと既存のカーネルで動く——PagedAttention論文の中核的洞察は決して複雑ではなく、難しいのはこれを2年間にわたって本番レベルに維持することだ。これがvLLMのソースコードが論文よりも読む価値がある理由でもある:アーキテクチャ思想は1枚の紙で説明できるが、エンジニアリングの完成度こそが堅牢な城(護城河)なのだ。
結論
PagedAttentionの貢献は一言に凝縮できる:KVキャッシュは「各リクエストごとの連続した配列」ではなく、「オンデマンドで割り当てられ、ブロック単位で共有されるプールされたリソース」だ。vLLMはOSの古い方法で大規模モデル推論の新しい問題を解決し、この一事で事実上の標準となった。大多数のチームにとって、選定に迷う必要はない——デフォルトでvLLMを使い、特殊な負荷があればSGLangを同じ土俵で負荷テストに引き入れる。推論システムを作りたいエンジニアにとって、そのソースコードは論文よりも良い教科書だ。
参考ソース
- vLLM GitHubリポジトリ(README、アーキテクチャドキュメント):https://github.com/vllm-project/vllm
- 論文:Efficient Memory Management for Large Language Model Serving with PagedAttention、SOSP 2023、arXiv:2309.06180(要旨、§2 無駄の分析、§6 評価)
- vLLM公式アーキテクチャドキュメント Architecture Overview(docs.vllm.ai、V1マルチプロセスアーキテクチャとソースコードインデックス)
- vLLM V1ソースコード:vllm/v1/core/block_pool.py(ブロックプールと空きキュー)、vllm/v1/core/kv_cache_utils.py(hash_block_tokensチェインハッシュ)、vllm/v1/core/kv_cache_manager.py(get_computed_blocksプレフィックスヒット)、vllm/v1/core/sched/scheduler.py(スケジューラメインループと_preempt_requestプリエンプション)、csrc/attention(paged attention CUDA/CUTLASSカーネル)
- Qwen2.5-72Bモデル設定(Hugging Face config.json:80層 / GQA 8 KVヘッド / ヘッド次元128、KVキャッシュVRAM計算の根拠)
- GitHubリポジトリメタデータと貢献者リスト、pypistats.org vllmダウンロード数(2026年9月)
- nano-vllm最小レプリカ(GitHub: GeeeekExplorer/nano-vllm、MIT):https://github.com/GeeeekExplorer/nano-vllm
- ソースコード解説二次資料(相互参照用):ブログ園「Nano-vLLM ソースコード解説」シリーズ、掘金「nano-vllmのKV CacheとPaged Attention」シリーズ、CSDN「大規模モデル推論エンジン vLLM 学習ノート」シリーズ(V1アーキテクチャとPrefix Caching編)
- SGLang GitHubリポジトリ(READMEとメタデータ):https://github.com/sgl-project/sglang;LMSYS blog 2024-01-17(RadixAttention公式見解)