
2026/02/02 21:52
**Nano‑vLLM:vLLMスタイルの推論エンジンが動作する仕組み**
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Nano‑vLLM は、約1,200行の Python コードで構成されるコンパクトな本格的運用レベルの推論エンジンであり、vLLM の主要機能(プレフィックスキャッシュ、テンソル並列化、CUDA グラフコンパイル、および torch 最適化)を実装しています。
エントリーポイントは
LLM クラスで、その中に generate メソッドがあります。プロンプトはトークン化されてシーケンスに変換され、次に producer‑consumer スケジューラ に渡されます:add_request は待機キューに入るシーケンスを生成し、ステップループがそれらをバッチで消費します。これにより prefill(全プロンプト)または decode(1 タークンずつ)のいずれかの処理が行われます。バッチングは総合スループットを向上させますが、遅延最速のリクエストまで待たされるため、1 つあたりのレイテンシが増加します。
Block Manager(CPU コントロールプレーン)は、GPU(データプレーン)上で KV キャッシュを割り当てます。シーケンスは固定長 256 トークンブロックに分割され、ハッシュ‑to‑block ハッシュングを介して接頭辞がブロックにマッピングされるため、共通の先頭を持つリクエスト間でプレフィックスキャッシュが可能になります。GPU メモリが枯渇した場合、スケジューラは実行中のシーケンスをプリエンプトし、再び待機キューに戻してリソースが解放されるまで待ちます。
Tensor parallelism は、リーダー‑ワーカー共有メモリプロトコルを用いてモデルを GPU に分散します:rank 0 が調整役で、rank 1–N‑1 がコマンドのポーリングを行います。
CUDA graphs は最も頻繁に使用されるバッチサイズについて事前にキャプチャされ、デコードステップ中のカーネル起動オーバーヘッドを削減します。
ロジットからサンプリングする際は、温度パラメータを用いて確率分布を形成し、低温での決定性と高温での創造性とのバランスを取ります。
この版はすべての重要ポイントを完全に反映しており、推論や曖昧な表現は追加していません。
本文
アーキテクチャ、スケジューリング、プロンプトからトークンへのパス
大規模言語モデル(LLM)を本番環境に導入する際は、推論エンジンが重要なインフラストラクチャの一部になります。OpenAI・Claude・DeepSeek など、あなたが利用するすべての LLM API は、このような推論エンジン上に構築されています。多くの開発者は高レベル API を通じて LLM と対話しますが、プロンプトがどのように処理されるか、リクエストがどのようにバッチ化され GPU リソースがどのように管理されるかを理解すると、システム設計上の意思決定に大きな影響を与えることがあります。
本シリーズは Nano‑vLLM を通じてこれらの内部構造を探ります。Nano‑vLLM は約 1,200 行程度の Python コードで、最も広く採用されているオープンソース推論エンジン vLLM の核心アイデアを凝縮した本番レベル実装です。DeepSeek に貢献した人物が開発し、DeepSeek‑V3 や R1 などのモデルの技術報告に名前が記載されています。コードベースは小さいものの、プレフィックスキャッシュ、テンソルパラレル、CUDA グラフコンパイル、torch コンパイレーション最適化といった vLLM を本番向けにする主要機能を実装しています。ベンチマークでは、完全版 vLLM と同等かそれ以上のスループットが得られ、数十種類のモデルアーキテクチャやハードウェアバックエンドをサポートする複雑さに悩まされることなく推論エンジン設計を理解できる理想的な窓口となります。
パート 1 では エンジニアリングアーキテクチャ、システム構成、リクエストの流れ、スケジューリング決定に焦点を当てます。実際のモデル計算はブラックボックスとして扱い、パート 2 で注意機構や KV キャッシュ内部、テンソルパラレルといった計算レベルの詳細へ踏み込みます。
主なフロー:プロンプトから出力まで
Nano‑vLLM の入り口はシンプルです。
LLM クラスに generate メソッドがあり、プロンプト配列とサンプリングパラメータを渡せば生成テキストが返ります。この簡易インターフェースの背後には、テキストをトークンへ変換し、計算を効率的にスケジューリングし、GPU リソースを管理する精巧なパイプラインがあります。
プロンプトからシーケンスへ
generate が呼び出されると、各プロンプト文字列はトークナイザー(モデル固有のコンポーネント)に渡されます。トークナイザーは自然言語を LLM が処理する基本単位であるトークンへ分割します。Qwen・LLaMA・DeepSeek といった異なるファミリーは別々のトークナイザーを使用するため、同じ長さのプロンプトでもモデルごとに生成されるトークン数が変わります。
トークナイザーは各プロンプトを シーケンス に変換します。シーケンスとは可変長のトークン ID 配列を表す内部データ構造で、以降のシステム全体に流れる作業単位となります。
プロデューサ―・コンシューマーパターン
ここがアーキテクチャの興味深い点です。各シーケンスを即座に処理する代わりに、システムは Scheduler を中心にプロデューサ―・コンシューマーパターンを採用しています。
が プロデューサー:プロンプトをシーケンスへ変換し Scheduler のキューに投入します。add_request- 別のステップループが コンシューマー で、Scheduler からバッチ化されたシーケンス群を取り出して処理します。
この分離は重要です。複数のシーケンスを蓄積しまとめて処理できるため、性能向上につながります。
バッチングとスループット・レイテンシトレードオフ
バッチングが重要な理由は、GPU 計算には固定オーバーヘッド(CUDA カーネルの初期化、CPU↔GPU メモリ転送、同期)が大きいからです。1 つずつ処理すると、そのオーバーヘッドを各リクエストで支払うことになります。複数シーケンスをバッチ化すれば、このオーバーヘッドを多くのリクエストに分散でき、全体スループットが劇的に向上します。
ただしバッチングにはトレードオフがあります。3 つのプロンプトを一括で処理すると、それぞれは他のシーケンスが完了するまで待機しなければならず、最終的な時間は遅い方のシーケンスに左右されます。
- 大きなバッチ → 高スループットだが個別リクエストのレイテンシが増大。
- 小さなバッチ → レイテンシ低減だがスループットが減少。
この基本的な緊張は、設定するバッチサイズパラメータで直接制御されます。
Prefill vs. Decode:生成の二段階
Scheduler に入る前に重要な区別を理解しておきましょう。LLM 推論は Prefill と Decode の 2 段階で構成されます。
- Prefill – 入力プロンプトを処理し、モデルの内部状態を構築します。このフェーズではユーザーには何も表示されません。
- Decode – 出力トークンを生成します。モデルは一度に 1 トークンずつ出力し、前回までの全トークンに依存します。この段階でテキストがストリームアウトします。
単一シーケンスの場合、Prefill が 1 回行われた後に多くの Decode ステップが続きます。Scheduler はこれらフェーズを区別しなければならず、Prefill は多数トークンを同時に処理するのに対し、Decode は 1 ステップごとに 1 トークンだけ処理します。
Scheduler の内部
Scheduler はどのシーケンスをいつ処理するかを決定します。2 つのキューを保持しています:
- Waiting Queue – 提出されたがまだ開始されていないシーケンス。
によって新しいシーケンスは常にここへ入ります。add_request - Running Queue – 現在処理中(Prefill か Decode)であるシーケンス。
Waiting Queue にシーケンスが入り、Scheduler は別のコンポーネント Block Manager と協力してリソースを割り当てます。割り当てられたら Running Queue に移動し、次の計算ステップで Running Queue からシーケンスを選択し、バッチ化してアクション指示(Prefill/Decode)と共に渡します。
リソース枯渇への対処
GPU メモリがいっぱいになるとどうなるでしょうか? KV キャッシュは中間計算結果を保持するため容量が限られています。Running Queue 内のシーケンスが次トークンのキャッシュを確保できない場合、Scheduler はそのシーケンスを preempt(中断)し、Waiting Queue の先頭に戻します。これによりリソースが解放されるとすぐに再開でき、他のシーケンスは進行できます。
シーケンスが完了(EOS トークンや最大長に到達)すると Scheduler は Running Queue から除外し、そのリソースを解放します。これで待機中のシーケンスが処理可能になります。
Block Manager:KV キャッシュ制御プレーン
Block Manager は vLLM のメモリ管理革新の核心です。理解するために「ブロック」という新しいリソース単位を導入します。
シーケンスからブロックへ
シーケンスは可変長(10 トークン~10,000 トークン)であるため、GPU メモリ管理として非効率です。Block Manager はシーケンスを固定サイズのブロック(デフォルト 256 トークン)に分割します。
- 700 トークンのシーケンスは 3 ブロック:2 個の完全ブロック(256×2=512)と 1 個の部分ブロック(188 トークン、68 スロット未使用)。
- 異なるシーケンスが同じブロックを共有することはありませんが、長いシーケンスは複数ブロックにまたがります。
Prefix キャッシュとハッシュ化
ここで工夫があります。各ブロックの内容をハッシュし、Block Manager は hash → ブロック ID のマッピングを保持します。新しいシーケンスが来ると、そのブロックごとのハッシュを計算し、キャッシュに既に存在するか確認します。
同一ハッシュのブロックがあれば、参照カウントを増やすだけで再利用できます。多くのリクエストが共通プレフィックス(チャットアプリのシステムプロンプトなど)を持つ場合、最初に 1 回だけ計算し、その後はキャッシュされた結果を共有します。
コントロールプレーン vs. データプレーン
Block Manager は CPU メモリ上にあり、メタデータ(割り当て済みブロック、参照カウント、ハッシュマップ)だけを追跡します。実際の KV キャッシュデータは GPU に存在します。Block Manager はコントロールプレーンであり、GPU メモリはデータプレーンです。この分離により、GPU メモリに触れることなく高速に割り当て判断が可能です。
ブロックを解放すると Block Manager は即座にフリーとしてマークしますが、GPU メモリ上の内容はゼロ化されません。再利用時に上書きされるだけで、不要なメモリ操作を回避できます。
Model Runner:実行とパラレル
Model Runner が GPU 上でモデルを実際に実行します。ステップループが Scheduler からバッチを取得すると、Model Runner に渡し、アクション(Prefill/Decode)も一緒に送ります。
テンソルパラレル通信
モデルが単一 GPU に収まらない場合、Nano‑vLLM はテンソルパラレル(TP)をサポートします。例:TP = 8 なら 8 台の GPU が協力して 1 本のモデルを実行。
通信アーキテクチャはリーダー–ワーカー構成です:
- Rank 0 (Leader) – ステップループからコマンドを受け取り、自分の部分を実行し、ワーカーと調整。
- Ranks 1‑N‑1 (Workers) – 共有メモリバッファを継続的にポーリングしてリーダーからのコマンドを検出。
Leader が「run」コマンドを受け取ると、メソッド名と引数を共有メモリへ書き込みます。ワーカーはこれを検知し、パラメータを読み取り、自身の GPU 上で同じ操作を実行します。各ワーカーは自身のランク情報から処理対象領域を計算できるため、シングルマシン多GPUセットアップでネットワークオーバーヘッドなしに効率的に動作します。
計算前の準備
Model Runner はアクションに応じて入力を準備します:
- Prepare Prefill – 可変長複数シーケンスをバッチ化し、注意機構計算のために累積長さを算出。
- Prepare Decode – 1 シーケンスあたり 1 トークンだけをバッチ化し、KV キャッシュへのアクセス位置とスロットマッピングを設定。
この段階で CPU 側トークンデータは GPU テンサーへ変換され、CPU ↔ GPU のメモリ転送が行われます。
CUDA Graphs:カーネル起動オーバーヘッド削減
Decode ステップでは 1 シーケンスあたり 1 トークンしか処理しないため、カーネル起動オーバーヘッドが相対的に大きくなります。CUDA Graphs は GPU 操作のシーケンスを一度記録し、異なる入力で再実行できる仕組みです。
Nano‑vLLM は 1, 2, 4, 8, 16…512 の一般的バッチサイズに対して CUDA Graph を事前キャプチャしており、Decode ステップで最小限の起動オーバーヘッドで実行できます。
サンプリング:ロジットからトークンへ
モデルは単一トークンを出力するわけではなく、語彙全体に対するロジット(確率分布)を返します。最終ステップはサンプリングであり、この分布から 1 トークンを選択します。
温度パラメータは分布の形状を調整します:
- 低温(0 に近い) – 分布が鋭くピーク化し、最も確率の高いトークンがほぼ必ず選ばれます。出力はより決定的で焦点が合います。
- 高温 – 分布がフラットになり、低確率トークンも選択されやすくなります。多様性と創造性が増します。
このサンプリングステップが LLM 出力の「ランダム性」を生み出し、同じプロンプトであっても異なる応答が得られる理由です。候補範囲から選択することで制御された変動を導入しています。
今後の展開
パート 2 では ブラックボックスであるモデル内部を解明します:
- トークンが隠れ状態へ、再びトークンへとどのように変換されるか。
- 注意機構とマルチヘッド注意の重要性。
- KV キャッシュが GPU メモリ上で物理的にどう配置されているか。
- デンス vs. MoE(Mixture of Experts)アーキテクチャの違い。
- テンソルパラレルが計算レベルでどのように機能するか。
これらを理解すれば、プロンプト文字列から生成テキストまでの全工程を完全に把握でき、隠された部分はもうありません。