Skip to content
Published on

vLLM 内部構造 (1) — リクエスト一つがトークンになるまでの全経路

シェア
Authors

このシリーズが扱うこと

LLMを自分でサービングした経験がある人は、大抵同じ順番で壁にぶつかる。最初はとにかく動くだけでもありがたく、次になぜこんなに遅いのかが気になり始め、最後にはなぜ突然メモリが溢れるのかを理解しなければならなくなる。この三つの問いへの答えは、すべてvLLMの内部にある。

このシリーズはvLLMを「速いサーバー」という結果としてではなく、構造として見る。全7回で構成され、第1回であるこの記事は地図に当たる。リクエストが一つ入ってきてトークンとして出ていくまでの経路を最初から最後まで一通りたどり、残りの回で各区間を拡大する。

始める前にバージョンの話を先にしておく必要がある。vLLMは変化が速く、内部を大きく書き直したことがある。公式のV1ガイドは、スケジューラ、KVキャッシュマネージャ、ワーカー、サンプラー、APIサーバーが再設計されたと明記している。そのため、このシリーズの説明はV1以降の構造を基準とする。古い記事の説明と食い違う場合、大抵はその記事がV0時代の話である。

内容は 2026-08-12 に公式ドキュメントとソースで確認しました。vLLM は変化が速いため、設定値と挙動は使用中のバージョンのドキュメントで再確認してください。

リクエスト一つの全経路

言葉で説明する前に、まず図で見る。

クライアントの HTTP リクエスト
[API サーバープロセス]   HTTP受信 · 入力処理 · トークン化
[エンジンコアプロセス]   スケジューラ + KVキャッシュマネージャ
        │             「このステップで誰に何トークン割り当てるか」
[GPU ワーカープロセス]   ModelRunner → モデル forward → ロジット
      サンプラー         ロジットから次のトークンを一つ選ぶ
        ├───────────▶ ストリーミングでクライアントに一部分を送る
        └───────────▶ まだ終わっていないリクエストは再びスケジューラへ

この図で最も重要なのは最後の矢印である。トークンが一つ出た後もリクエストは終わらず、再びスケジューラに戻る。一つのリクエストを最後まで処理してから次のリクエストを見る構造ではなく、生きているすべてのリクエストが毎ステップ少しずつ前進する。vLLMの性能の話は、結局このループをいかに隙間なく埋めるかという問題に帰着する。

APIサーバー: リクエストをエンジンの言語に変換する場所

vLLMにはエントリーポイントが二つある。公式のアーキテクチャドキュメントによると、オフライン推論は LLM クラスが担当し、オンラインサービングは vllm serve コマンドまたはOpenAI互換APIサーバーが担当する。そしてこのOpenAI互換サーバーは AsyncLLMEngine を使う。非同期ラッパーがバックグラウンドループを回しているため、HTTPリクエストが複数同時に来ても、それぞれが互いを待たない。

ここで行うこと自体は地味である。チャットテンプレートを適用し、文字列をトークン列に変換し、サンプリング設定を整理してエンジンが理解できるリクエストオブジェクトにする。ただし、この段階ですでに性能が分かれる。プロンプトをどう組み立てたかが第5回で扱うプレフィックスキャッシュのヒット可否を左右し、入力の長さが第6回で扱う長さの上限にそのまま関わってくる。

エンジンコア: スケジューラとKVキャッシュマネージャ

V1のプロセス構成は公式ドキュメントに次のように整理されている。HTTPを受け取るAPIサーバープロセスがあり、スケジューラを回してKVキャッシュを管理するエンジンコアプロセスがデータ並列ランクごとに一つあり、GPU一枚につきワーカープロセスが一つある。ドキュメントが挙げる例では、GPU4枚構成のデプロイはAPIサーバー1 + エンジンコア1 + GPUワーカー4、合計6プロセスになる。

プロセスを分けた理由は単純である。HTTPパースとトークン化はPythonがCPUを使う作業であり、モデル実行はGPUを使う作業である。この二つが同じプロセスにあると、CPU側の作業がGPUループを止めてしまう。実際に処理量が出ないデプロイを調べてみると、GPU使用率は低いのにCPUコアが一つだけ100パーセントになっているケースがよくある。

エンジンコアの中で、スケジューラとKVキャッシュマネージャは一体となって動く。スケジューラが「このリクエストを今回のステップに入れる」と決めるには、それに見合うKVキャッシュブロックを実際に確保できる必要があるからである。確保できなければ、スケジューラはすでに動いていたリクエストを後退させることまで行う。このプリエンプション動作が第4回のテーマである。

ワーカーとサンプラー: 実際にGPUを扱う層

アーキテクチャドキュメントの定義は簡潔である。 Worker はモデル推論を実行するプロセスであり、 ModelRunner はモデルをロードして実行する役割を持ち、その中の Model が実際のtorchモジュールインスタンスである。

ワーカーがforwardを一度実行すると、バッチ内の各リクエストについて次のトークンのロジットが出てくる。サンプラーはここにtemperature、top_p、top_kといった設定を適用してトークンを一つ選ぶ。リクエストごとにサンプリング設定が異なっていても同じバッチに混ざれるという点が重要である。バッチはモデルの計算を共有するだけであり、何を選ぶかはリクエストごとに個別に決まる。

設定が一つのオブジェクトに集約される理由

ドキュメントが強調する設計の一つが VllmConfig である。必要な情報をすべて詰め込んだ設定オブジェクトを作っておき、それを渡すという発想である。おかげで、モデルを全部ロードした後で重みを修正するのではなく、初期化の時点でシャーディングと量子化を同時に適用できる。巨大なモデルではこの違いが決定的になる。GPU一枚に収まらないモデルを、いったんロードしてから分割することはできないからである。

この構造は運用にも影響する。 --max-model-len--gpu-memory-utilization のような値が起動時点で固定され、以降のすべてに影響を与える理由はここにある。そのため、vLLMのチューニングはランタイムでの調整ではなく、大部分が起動引数の設計になる。

これで地図は描けた。次回からは、この経路の中で最も多くの人がつまずく区間を一つずつ拡大していく。

試してみる

参考資料