AI・機械学習
vLLMの内部:高スループットLLM推論システムの解剖 (2025)
Inside vLLM: Anatomy of a High-Throughput LLM Inference System (2025) (aleksagordic.com)
要約
この記事は、高スループットなLLM推論システムを支えるvLLMのコアコンポーネントと高度な機能を解説します。特に、vLLMのアーキテクチャを詳細に分解し、スケジューリング、ページドアテンション、連続バッチ処理などの基本から、チャンク化プリフィル、プレフィックスキャッシング、スケーリングアップ、サービングレイヤー、ベンチマークといった高度な機能までを段階的に説明します。これは、LLMエンジンの仕組みに興味がある読者や、vLLMへの貢献を考えている開発者を対象としています。
全文翻訳
この記事では、最新の高スループットLLM推論システムを構成するすべてのコアシステムコンポーネントと高度な機能を段階的に紹介します。特に、vLLM [1] がどのように機能するかを分解します。
この記事はシリーズの最初のものです。詳細に踏み込む前に、まず全体像を広く説明し、その後で詳細を重ねていく(逆ピラミッドアプローチに従って)ことで、細部に溺れることなく、完全なシステムの正確なハイレベルなメンタルモデルを形成できるようにします。
後の記事では、特定のサブシステムについて詳しく説明します。
この記事は5つのパートで構成されています。
LLMエンジン & エンジンコア: vLLMの基本(スケジューリング、ページドアテンション、連続バッチ処理など)
高度な機能: チャンク化プリフィル、プレフィックスキャッシング、ガイド付き・推論的デコーディング、非集約P/D
スケーリングアップ: シングルGPUからマルチGPU実行へ
サービングレイヤー: 分散/並行Webスキャフォールディング
ベンチマークと自動チューニング: レイテンシとスループットの測定
📝注
分析はコミット 42172ad (2025年8月9日) に基づいています。
対象読者: 最新のLLMエンジンの仕組みに興味がある人、およびvLLM、SGLangなどに貢献したいと考えている人。
V1エンジンに焦点を当てます。V0(現在は非推奨)も調査しましたが、プロジェクトがどのように進化してきたかを理解する上で価値があり、多くの概念は依然として引き継がれています。
LLMエンジン / エンジンコアの最初のセクションは、少し圧倒的で退屈かもしれませんが、ブログの残りの部分には plenty の例とビジュアルがあります。:)
LLMエンジン & エンジンコア
vLLMの基本的な構成要素はLLMエンジンです。それ自体で、高スループットの推論を可能にしますが、オフライン設定でのみ可能です。まだWeb経由で顧客に提供することはできません。
実行例として、以下のオフライン推論スニペットを使用します(basic.pyから適応)。
from vllm import LLM, SamplingParams
prompts = [
"Hello, my name is",
"The president of the United States is",
]
sampling_params = SamplingParams(temperature=0.8, top_p=0.95)
def main():
llm = LLM(model="TinyLlama/TinyLlama-1.1B-Chat-v1.0")
outputs = llm.generate(prompts, sampling_params)
if __name__ == "__main__":
main()
📝環境変数:
VLLM_USE_V1="1" # エンジンV1を使用しています
VLLM_ENABLE_V1_MULTIPROCESSING="0" # シングルプロセスで実行しています
この構成は以下の通りです。
オフライン(Web/分散システムスキャフォールディングなし)
同期式(すべての実行は単一のブロッキングプロセスで発生します)
シングルGPU(データ/モデル/パイプライン/エキスパート並列なし; DP/TP/PP/EP = 1)
標準トランスフォーマー [2] を使用(Jambaのようなハイブリッドモデルをサポートするには、より複雑なハイブリッドKVキャッシュメモリ割り当てが必要です)
ここから、オンライン、非同期、マルチGPU、マルチノード推論システムへと徐々に構築していきますが、依然として標準トランスフォーマーを提供します。
この例では、2つのことを行います。
エンジンをインスタンス化する
指定されたプロンプトからサンプリングするために、それにgenerateを呼び出す
コンストラクタの分析から始めましょう。
LLMエンジンコンストラクタ
エンジンの主なコンポーネントは以下の通りです。
vLLM config(モデル、キャッシュ、並列処理などを設定するためのすべてのノブを含みます)
プロセッサ(検証、トークン化、処理を通じて、生の入力をEngineCoreRequestsに変換します)
エンジンコアクライアント(実行中の例ではInprocClientを使用しています。これは基本的にEngineCoreと等価です。スケールでの提供を可能にするDPLBAsyncMPClientへと徐々に構築していきます)
出力プロセッサ(生のEngineCoreOutputsをユーザーが見るRequestOutputに変換します)
📝注:
V0エンジンは非推奨になったため、クラス名や詳細は変更される可能性があります。正確なシグネチャよりもコアのアイデアを強調します。それらの詳細のすべてではなく、一部を抽象化します。
エンジンコア自体は、いくつかのサブコンポーネントで構成されています。
モデルエグゼキュータ(モデルのフォワードパスを駆動します。現在、単一GPU上の単一Workerプロセスを持つUniProcExecutorを扱っています。)マルチGPUをサポートするMultiProcExecutorへと徐々に構築していきます。
構造化出力マネージャー(ガイド付きデコーディングに使用されます。後でカバーします。)
スケジューラ(このステップでどのリクエストを実行するかを決定します。)さらに以下を含みます。
ポリシー設定 - FCFS(先着順)または優先度(優先度の高いリクエストが先に処理される)のいずれかになります。
待機キューと実行キュー
KVキャッシュマネージャー - ページドアテンション [3] の心臓部
KVキャッシュマネージャーは、利用可能なKVキャッシュブロックのプールであるfree_block_queueを維持します(多くの場合、VRAMサイズとブロックサイズに応じて数百千単位です)。ページドアテンション中、ブロックはトークンを計算されたKVキャッシュブロックにマッピングするインデックス構造として機能します。
このセクションで説明するコアコンポーネントとその関係
標準トランスフォーマーレイヤー(非MLA [4])のブロックサイズは次のように計算されます。
2 (キー/バリュー) * block_size (デフォルト=16) * num_kv_heads * head_size * dtype_num_bytes (例: bf16の場合は2)
モデルエグゼキュータの構築中に、Workerオブジェクトが作成され、3つの主要な手順が実行されます。(後で、MultiProcExecutorを使用すると、これらの同じ手順が異なるGPU上の各ワーカープロセスで独立して実行されます。)
デバイスの初期化:
CUDAデバイス(例: "cuda:0")をワーカーに割り当て、モデルのdtypeがサポートされているか(例: bf16)を確認します。
要求されたgpu_memory_utilization(例: 0.8 → 総VRAMの80%)を考慮して、十分なVRAMがあることを確認します。
分散設定(DP / TP / PP / EPなど)を設定します。
model_runnerをインスタンス化します(サンプラー、KVキャッシュ、および入力ID、位置などのフォワードパスバッファを保持します)。
InputBatchオブジェクトをインスタンス化します(CPU側のフォワードパスバッファ、KVキャッシュインデックス用のブロックテーブル、サンプリングメタデータなどを保持します)。
モデルのロード:
モデルアーキテクチャをインスタンス化します。
モデルの重みをロードします。
model.eval()(PyTorchの推論モード)を呼び出します。
オプション: モデルにtorch.compile()を呼び出します。
KVキャッシュの初期化
レイヤーごとのKVキャッシュ仕様を取得します。歴史的にはこれは常にFullAttentionSpec(均質なトランスフォーマー)でしたが、ハイブリッドモデル(スライディングウィンドウ、JambaのようなTransformer/SSM)ではより複雑になりました(Jenga [5] を参照)。
ダミー/プロファイリングフォワードパスを実行し、GPUメモリのスナップショットを取得して、利用可能なVRAMにいくつのKVキャッシュブロックが収まるかを計算します。
アテンションレイヤーにKVキャッシュテンソルを割り当て、リシェイプし、バインドします。
アテンションメタデータを準備します(例: バックエンドをFlashAttentionに設定)。これは後でフォワードパス中にカーネルによって消費されます。
--enforce-eagerが提供されていない場合、ウォームアップバッチサイズのそれぞれに対してダミー実行を行い、CUDAグラフをキャプチャします。CUDAグラフは、GPUワークのシーケンス全体をDAGに記録します。後でフォワードパス中に、プリベイクされたグラフを起動/再生し、カーネル起動オーバーヘッドをカットすることでレイテンシを改善します。
ここでは多くの低レベルの詳細を抽象化しましたが、これらは後続のセクションで繰り返し参照するため、ここで紹介するコアピースです。
エンジンが初期化されたので、generate関数に進みましょう。
Generate関数
最初のステップは、リクエストを検証してエンジンにフィードすることです。各プロンプトについて、以下を行います。
一意のリクエストIDを作成し、その到着時刻をキャプチャします。
入力プリプロセッサを呼び出し、プロンプトをトークン化して、プロンプト、prompt_token_ids、およびタイプ(text、tokens、embedsなど)を含む辞書を返します。
この情報をEngineCoreRequestにパックし、優先度、サンプリングパラメータ、その他のメタデータを追加します。
リクエストをエンジンコアに渡します。エンジンコアはそれをRequestオブジェクトでラップし、ステータスをWAITINGに設定します。このリクエストはスケジューラの待機キューに追加されます(FCFSの場合はappend、優先度の場合はheap-push)。
この時点でエンジンへの入力は完了し、実行を開始できます。同期エンジン例では、これらの初期プロンプトのみが処理されます。実行中に新しいリクエストを注入するメカニズムはありません。対照的に、非同期エンジンはこれをサポートします(いわゆる連続バッチ処理 [6])。各ステップの後、新しいリクエストと古いリクエストの両方が考慮されます。
フォワードパスはバッチを単一のシーケンスにフラット化し、カスタムカーネルが効率的に処理するため、連続バッチ処理は同期エンジンでも基本的にサポートされています。
次に、処理するリクエストがある限り、エンジンは繰り返しstep()関数を呼び出します。各ステップは3つのステージで構成されます。
スケジュール: このステップで実行するリクエストを選択します(デコード、および/または(チャンク化された)プリフィル)。
フォワードパス: モデルを実行します。