コードリーディング: vLLM & LMCache
LLM推論サービングに関連するOSSのコードリーディング結果を構造化して蓄積するプロジェクト。
対象OSS
| OSS | ソースコード | 概要 | Phase |
|---|---|---|---|
| vLLM | target/vllm/ | LLM推論サービングエンジン | Phase 2 |
| LMCache | target/LMCache/ | KVキャッシュ保存・共有・再利用ライブラリ | Phase 0a |
プロジェクト横断
- 横断調査 — 複数OSSにまたがる調査報告
vLLM
LLM推論サービングエンジン vLLM のコードリーディング。
- ソースコード:
target/vllm/ - 現在のPhase: Phase 2(コンポーネント別深堀り)
テキスト推論データフロー
深度: [MEDIUM] 確信度: [VERIFIED] 最終更新: 2026-02-11 (Phase 2bでマルチモーダル差分追加)
概要
テキスト推論リクエストは、APIエントリポイントからエンジン層を経てGPUで実行され、生成されたトークンがデトークナイズされてユーザーに返却される。フロー全体は5つの境界データ構造(EngineCoreRequest → SchedulerOutput → ModelRunnerOutput → EngineCoreOutput → RequestOutput)で区切られ、ZMQ IPCによるプロセス分離とasyncioによる非同期パイプラインで高スループットを実現する。
フロー全体図
graph TD
subgraph フロントエンドプロセス
API["API Server / LLM"]
AsyncLLM["AsyncLLM<br>generate() / add_request()"]
IP["InputProcessor<br>process_inputs()"]
OP["OutputProcessor<br>process_outputs()"]
Client["EngineCoreClient<br>AsyncMPClient"]
end
subgraph バックエンドプロセス ["EngineCore プロセス"]
EC["EngineCore<br>step()"]
Sched["Scheduler<br>schedule()"]
KV["KVCacheManager<br>allocate_slots()"]
Exec["Executor<br>execute_model()"]
Worker["Worker"]
MR["GPUModelRunner<br>execute_model()"]
end
API -->|"prompt, params"| AsyncLLM
AsyncLLM -->|"prompt, params"| IP
IP -->|"EngineCoreRequest"| AsyncLLM
AsyncLLM -->|"EngineCoreRequest"| Client
Client -->|"ZMQ ROUTER\nmsgpack"| EC
EC --> Sched
Sched -->|"allocate_slots()"| KV
Sched -->|"SchedulerOutput"| EC
EC -->|"SchedulerOutput"| Exec
Exec -->|"MessageQueue\n共有メモリ"| Worker
Worker --> MR
MR -->|"ModelRunnerOutput"| Worker
Worker -->|"ModelRunnerOutput"| Exec
Exec -->|"ModelRunnerOutput"| EC
EC -->|"update_from_output()"| Sched
Sched -->|"EngineCoreOutputs"| EC
EC -->|"ZMQ PUSH\nmsgpack"| Client
Client -->|"EngineCoreOutputs"| OP
OP -->|"RequestOutput"| AsyncLLM
AsyncLLM -->|"RequestOutput"| API
境界データ構造
フローは以下の5つのデータ構造で区切られる。各構造はプロセス間またはコンポーネント間の境界を定義する。
EngineCoreRequest
フロントエンド → バックエンドの境界。ユーザー入力を正規化した内部表現。
参照: target/vllm/vllm/v1/engine/__init__.py:55 (EngineCoreRequest)
| フィールド | 型 | 説明 |
|---|---|---|
request_id | str | 内部リクエストID(外部IDに8文字ランダムサフィックス付与) |
prompt_token_ids | list[int] | None | トークナイズ済みプロンプト |
mm_features | list[MultiModalFeatureSpec] | None | マルチモーダル入力(テキスト推論ではNone) |
sampling_params | SamplingParams | None | サンプリングパラメータ(clone済み) |
eos_token_id | int | None | 終了トークンID |
arrival_time | float | リクエスト到着時刻 |
lora_request | LoRARequest | None | LoRAアダプタ情報 |
priority | int | 優先度(デフォルト0) |
data_parallel_rank | int | None | データ並列ランク指定 |
msgspec.Struct を継承し、array_like=True + omit_defaults=True で効率的にmsgpackシリアライズされる。
SchedulerOutput
Scheduler → Executor の境界。各ステップのスケジュール結果を含む。
参照: target/vllm/vllm/v1/core/sched/output.py:184 (SchedulerOutput)
| フィールド | 型 | 説明 |
|---|---|---|
scheduled_new_reqs | list[NewRequestData] | 初回スケジュールされたリクエスト(フルデータ) |
scheduled_cached_reqs | CachedRequestData | 既スケジュール済みリクエスト(差分のみ) |
num_scheduled_tokens | dict[str, int] | リクエストごとのスケジュールトークン数 |
total_num_scheduled_tokens | int | 合計スケジュールトークン数 |
scheduled_spec_decode_tokens | dict[str, list[int]] | Speculative Decoding用トークン |
scheduled_encoder_inputs | dict[str, list[int]] | エンコーダ入力インデックス(マルチモーダル) |
num_common_prefix_blocks | list[int] | 共通プレフィックスブロック数(Cascade Attention用) |
finished_req_ids | set[str] | このステップで完了したリクエストID |
free_encoder_mm_hashes | list[str] | 解放するエンコーダキャッシュのmm_hash |
preempted_req_ids | set[str] | None | プリエンプションされたリクエスト |
has_structured_output_requests | bool | 構造化出力リクエストの有無 |
pending_structured_output_tokens | bool | Grammar bitmask準備状態 |
num_invalid_spec_tokens | dict[str, int] | None | 無効スペキュレーショントークン数 |
kv_connector_metadata | KVConnectorMetadata | None | KV Transfer メタデータ |
ec_connector_metadata | ECConnectorMetadata | None | EC Transfer メタデータ |
NewRequestData は初回スケジュール時のフルデータ(プロンプトトークン、サンプリングパラメータ、ブロックID等)を含む。CachedRequestData は既スケジュール済みリクエストの差分(新規ブロックID、新トークンID、計算済みトークン数の更新)のみを含み、プロセス間通信コストを最小化する。
ModelRunnerOutput
GPUModelRunner → EngineCore の境界。モデル推論結果を含む。
参照: target/vllm/vllm/v1/outputs.py:160 (ModelRunnerOutput)
| フィールド | 型 | 説明 |
|---|---|---|
req_ids | list[str] | バッチ内のリクエストID一覧 |
req_id_to_index | dict[str, int] | リクエストID → バッチインデックス |
sampled_token_ids | list[list[int]] | サンプリング済みトークンID [num_reqs, num_generated] |
logprobs | LogprobsLists | None | 生成トークンの対数確率 |
prompt_logprobs_dict | dict[str, LogprobsTensors | None] | プロンプトトークンの対数確率 |
pooler_output | list[Tensor | None] | None | プーリング出力(埋め込みモデル用) |
kv_connector_output | KVConnectorOutput | None | KV Transfer出力 |
ec_connector_output | ECConnectorOutput | None | EC Transfer出力 |
num_nans_in_logits | dict[str, int] | None | logits内のNaN数 |
cudagraph_stats | CUDAGraphStat | None | CUDAGraph実行統計 |
Worker→Executorへの転送ではPythonリスト形式を使用し、torch.Tensorの高コストなシリアライゼーションを回避する。
EngineCoreOutput
バックエンド → フロントエンドの境界。リクエスト単位の推論結果。
参照: target/vllm/vllm/v1/engine/__init__.py:130 (EngineCoreOutput)
| フィールド | 型 | 説明 |
|---|---|---|
request_id | str | 対応するリクエストID |
new_token_ids | list[int] | 新たに生成されたトークンID |
finish_reason | FinishReason | None | 完了理由(stop/length/abort/error) |
new_logprobs | LogprobsLists | None | 生成トークンのlogprobs |
num_cached_tokens | int | プレフィックスキャッシュヒット数 |
EngineCoreOutputs(複数形)がこれをlist[EngineCoreOutput]としてバッチ化し、scheduler_statsやタイムスタンプと共にZMQ経由で送信される。
参照: target/vllm/vllm/v1/engine/__init__.py:176 (EngineCoreOutputs)
RequestOutput
OutputProcessor → API の境界。ユーザーに返却される最終出力。
参照: target/vllm/vllm/outputs.py:86 (RequestOutput)
| フィールド | 型 | 説明 |
|---|---|---|
request_id | str | 外部リクエストID(クライアントが指定したID) |
prompt | str | None | 元のプロンプト文字列 |
prompt_token_ids | list[int] | None | トークナイズ済みプロンプト |
prompt_logprobs | PromptLogprobs | None | プロンプトトークンの対数確率 |
outputs | list[CompletionOutput] | サンプルごとの出力(n>1で複数) |
finished | bool | リクエスト完了フラグ |
metrics | RequestStateStats | None | レイテンシ等の統計情報 |
num_cached_tokens | int | None | プレフィックスキャッシュヒット数 |
kv_transfer_params | dict[str, Any] | None | KV Transfer情報(完了時) |
CompletionOutput (target/vllm/vllm/outputs.py:23) は各サンプルの出力を表す:
| フィールド | 型 | 説明 |
|---|---|---|
index | int | サンプルインデックス |
text | str | デトークナイズ済みテキスト |
token_ids | GenericSequence[int] | 生成トークンID列 |
cumulative_logprob | float | None | 累積対数確率 |
logprobs | SampleLogprobs | None | 各トークンのlogprobs |
finish_reason | str | None | 完了理由(“stop” / “length”) |
stop_reason | int | str | None | 停止トークン/文字列 |
出力モード(RequestOutputKind、target/vllm/vllm/sampling_params.py:108):
CUMULATIVE: 毎回全出力を返す(デフォルト)DELTA: 差分のみ返す(ストリーミング向け)FINAL_ONLY: 完了時のみ返す
上流パス: リクエスト受信 → EngineCore
エントリポイント (LLM / AsyncLLM)
vLLMには同期パス(LLM)と非同期パス(AsyncLLM)の2つのエントリポイントがある。内部的にはどちらもInputProcessorとEngineCoreClientを使用する。
非同期パス(主パス): AsyncLLM
APIサーバー(OpenAI互換API等)が使用する主要パス。
参照: target/vllm/vllm/v1/engine/async_llm.py:71 (AsyncLLM)
AsyncLLM.generate(prompt, sampling_params, request_id) # L537
│
├─ add_request(request_id, prompt, params) # L286
│ ├─ input_processor.process_inputs(prompt, params) # L364
│ │ → EngineCoreRequest を生成
│ ├─ input_processor.assign_request_id(request) # L378
│ │ → 内部IDを付与(外部ID + 8文字ランダムサフィックス)
│ ├─ output_processor.add_request(request, ...) # L423
│ │ → フロントエンド側でリクエストを登録
│ └─ engine_core.add_request_async(request) # L426
│ → ZMQ経由でバックエンドへ送信
│
└─ while not finished: # L586
out = q.get_nowait() or await q.get() # L589
yield out # L596
generate()はAsyncGeneratorで、バックグラウンドのoutput_handlerタスクがEngineCoreからの出力をRequestOutputCollectorキューにpushし、generate()がそれをyieldする。
output_handler(バックグラウンドタスク):
参照: target/vllm/vllm/v1/engine/async_llm.py:647 (_run_output_handler)
output_handler(): # L662
while True:
outputs = await engine_core.get_output_async() # L666
for chunk in outputs.outputs: # L677
output_processor.process_outputs(chunk, ...) # L681
→ RequestOutputをキューにpush
if reqs_to_abort:
await engine_core.abort_requests_async(...) # L693
同期パス: LLM
オフライン推論(バッチ処理)で使用される。
参照: target/vllm/vllm/entrypoints/llm.py:396 (generate)
LLM.generate(prompts, sampling_params) # L396
→ _run_completion(prompts, params) # L449
→ _add_request(prompt, params) × N # L1850
│ ├─ input_processor.process_inputs(prompt, params) # L1879
│ └─ llm_engine.add_request(request_id, request, ...) # L1889
→ _run_engine() # L1900
while has_unfinished_requests(): # L1918
step_outputs = llm_engine.step() # L1919
同期パスとの主な違い:
LLMは_run_engine()でポーリングループを回す(AsyncGeneratorではない)llm_engine(=AsyncLLMのラッパー)のstep()を直接呼ぶ- プログレスバー(tqdm)でバッチ処理の進捗を表示
入力処理 (InputProcessor)
InputProcessorはユーザー入力(テキストプロンプト、パラメータ)をEngineCoreRequestに変換する。
参照: target/vllm/vllm/v1/engine/input_processor.py:56 (InputProcessor)
InputProcessor.process_inputs(request_id, prompt, params) # L521
├─ _validate_lora(lora_request) # L535
├─ _validate_params(params) # L536
├─ data_parallel_rank の範囲チェック # L542
├─ arrival_time 設定(未指定なら time.time()) # L548
│
├─ input_preprocessor.preprocess(prompt, ...) # L581
│ → テキストをトークナイズ(tokenizer.encode())
│ → ProcessorInputs を返す
│
├─ split_enc_dec_inputs(processed_inputs) # L597
│ → エンコーダ/デコーダ入力を分離
│
├─ SamplingParams の正規化 # L608-623
│ ├─ params.clone() # L612
│ ├─ max_tokens 未設定時: max_model_len - seq_len # L614-618
│ ├─ update_from_generation_config() # L619
│ └─ update_from_tokenizer() # L623
│
└─ EngineCoreRequest を構築して返す # L656-671
テキスト推論の場合、マルチモーダル関連処理(L630-654)はスキップされる(mm_featuresはNone)。
プロセス間通信 (EngineCoreClient / ZMQ IPC)
EngineCoreClientはフロントエンドプロセスとバックエンドプロセス(EngineCore)間のZMQ IPC通信を担当する。
参照: target/vllm/vllm/v1/engine/core_client.py:63 (EngineCoreClient)
クライアント階層
| クラス | 用途 | トランスポート |
|---|---|---|
EngineCoreClient (ABC) | 抽象インターフェース | — |
InprocClient | インプロセス(デバッグ用) | 直接呼び出し |
SyncMPClient | 同期マルチプロセス(LLM用) | ZMQ同期 |
AsyncMPClient | 非同期マルチプロセス(AsyncLLM用) | ZMQ非同期 |
DPAsyncMPClient | データ並列(外部LB) | 複数ZMQ |
DPLBAsyncMPClient | データ並列(内部LB) | 複数ZMQ |
参照: target/vllm/vllm/v1/engine/core_client.py:442 (MPClient)
ZMQソケット構成
フロントエンド バックエンド
┌──────────────┐ ┌──────────────┐
│ AsyncMPClient│ │ EngineCore │
│ │ │ │
│ input_socket ├─── ROUTER ──→│ (受信) │
│ (zmq.ROUTER) │ msgpack │ │
│ │ │ │
│ output_socket│←── PULL ─────┤ (送信) │
│ (zmq.PULL) │ msgpack │ │
└──────────────┘ └──────────────┘
- シリアライゼーション:
MsgpackEncoder/MsgpackDecoder(msgspecライブラリ)EngineCoreRequestのシリアライズ → 入力ソケット経由で送信EngineCoreOutputsのデシリアライズ ← 出力ソケット経由で受信
- 非同期出力受信:
process_outputs_socket()タスクがZMQソケットをポーリングし、受信したOutputsをasyncio.Queueにpush
参照: target/vllm/vllm/v1/engine/core_client.py:822 (AsyncMPClient)
EngineCore側のリクエスト受信
EngineCore.add_request()はリクエストをバリデーションしてSchedulerに登録する。
参照: target/vllm/vllm/v1/engine/core.py:288 (add_request)
EngineCore.add_request(request) # L288
├─ request_id の型チェック # L295
├─ pooling_params のバリデーション # L300
├─ kv_transfer_params の互換性チェック # L311
└─ scheduler.add_request(request) # L319
EngineCore.step() (コアループ概要)
参照: target/vllm/vllm/v1/engine/core.py:389 (step)
EngineCore.step() # L389
├─ scheduler.schedule() → SchedulerOutput # L404
├─ executor.execute_model() → Future[ModelRunnerOutput] # L405
├─ grammar_output 取得 # L406
├─ future.result() → ModelRunnerOutput # L411
├─ sample_tokens()(非同期スケジューリング時) # L413
└─ scheduler.update_from_output() → EngineCoreOutputs # L418
コアループ: EngineCore.step()
EngineCoreのstep()メソッドは、各ステップで schedule → execute → update のサイクルを実行し、待機中のリクエストから生成トークンを生産する。
step() 実行フロー
参照: target/vllm/vllm/v1/engine/core.py:389 (step)
EngineCore.step() # L389
│
├─ if _scheduler_paused: return {}, False # L397
├─ if not scheduler.has_requests(): return {}, False # L402
│
├─ 1. scheduler_output = scheduler.schedule() # L404
│ → SchedulerOutput
│ (RUNNINGリクエストの予算割当 → WAITINGリクエストの受け入れ
│ → KVキャッシュブロック確保 → SchedulerOutput構築)
│
├─ 2. future = executor.execute_model( # L405
│ scheduler_output, non_block=True)
│ → Future[ModelRunnerOutput | None]
│ (非ブロッキング。ワーカープロセスで並行実行)
│
├─ 3. grammar_output = scheduler.get_grammar_bitmask( # L406
│ scheduler_output)
│ (構造化出力有効時のみ使用)
│
├─ 4. model_output = future.result() # L411
│ → ModelRunnerOutput(ブロッキング待機)
│
├─ 5. if model_output is None: # L413
│ model_output = executor.sample_tokens(grammar_output)
│ (非同期スケジューリング時: execute_modelとsamplingが分離)
│
├─ 6. _process_aborts_queue() # L417
│
└─ 7. engine_core_outputs = scheduler.update_from_output( # L418
│ scheduler_output, model_output)
│ → dict[int, EngineCoreOutputs]
│ (生成トークンの追加、完了判定、出力構築)
│
└─ return (engine_core_outputs, # L422
total_num_scheduled_tokens > 0)
Scheduler と KVCacheManager の相互作用
sequenceDiagram
participant EC as EngineCore
participant S as Scheduler
participant KV as KVCacheManager
participant Ex as Executor
EC->>S: schedule()
Note over S: Phase 1: RUNNINGリクエスト処理
loop 各RUNNINGリクエスト
S->>KV: allocate_slots(request, num_new_tokens)
KV-->>S: KVCacheBlocks or None
alt 割り当て失敗 (None)
S->>KV: free(低優先度request)
Note over S: プリエンプション → 再試行
end
end
Note over S: Phase 2: WAITINGリクエスト受け入れ
loop 各WAITINGリクエスト
S->>KV: get_computed_blocks(request)
KV-->>S: (cached_blocks, num_hits)
S->>KV: allocate_slots(request, num_new_tokens, ...)
KV-->>S: KVCacheBlocks or None
alt 割り当て失敗 (None)
Note over S: break(ループ終了)
end
end
Note over S: Phase 3: SchedulerOutput構築
S-->>EC: SchedulerOutput
EC->>Ex: execute_model(scheduler_output)
Ex-->>EC: Future[ModelRunnerOutput]
EC->>EC: future.result()(待機)
EC->>S: update_from_output(scheduler_output, model_output)
Note over S: トークン追加、完了判定
S-->>EC: dict[int, EngineCoreOutputs]
Scheduler.schedule() の3フェーズ
参照: target/vllm/vllm/v1/core/sched/scheduler.py:321 (schedule)
schedule() は Unified Compute Model を採用し、Prefill/Decodeを区別せず num_computed_tokens の進捗で統一的にトークンを割り当てる。
| フェーズ | 行 | 対象 | 処理 |
|---|---|---|---|
| Phase 1 | L350-517 | RUNNINGリクエスト | トークン予算割当。ブロック不足時はプリエンプション |
| Phase 2 | L532-800 | WAITINGリクエスト | 新規受け入れ。プレフィックスキャッシュ検索 + ブロック割当 |
| Phase 3 | L827-896 | 出力構築 | NewRequestData + CachedRequestData → SchedulerOutput |
トークン予算: token_budget = max_num_scheduled_tokens(ステップあたり上限)で、各リクエストのスケジュール時に消費される。
詳細は Scheduler サマリー を参照。
KVCacheManager のブロック割り当て
参照: target/vllm/vllm/v1/core/kv_cache_manager.py:206 (allocate_slots)
allocate_slots() は以下のブロック配置に基づいてGPUメモリブロックを確保する:
| comp | new_comp | ext_comp | new | lookahead |
|<------ 既計算トークン ------>|<-- 新規計算対象 -->|
|<- 割り当て対象 ->|
- 成功時:
KVCacheBlocks(割り当てたブロック情報)を返す - 失敗時:
Noneを返す → Schedulerがプリエンプション(RUNNING)またはスキップ(WAITING)
プレフィックスキャッシュ検索は get_computed_blocks() で行い、過去に計算済みのブロックを再利用する。
詳細は KVCacheManager サマリー を参照。
update_from_output() → EngineCoreOutputs
参照: target/vllm/vllm/v1/core/sched/scheduler.py:1241 (update_from_output)
ModelRunnerOutputを受けてSchedulerの状態を更新し、クライアントに返すEngineCoreOutputsを構築する。
update_from_output(scheduler_output, model_runner_output)
for each scheduled request:
├─ Speculative Decodingリジェクション処理
│ → 不採用分の num_computed_tokens 巻き戻し
├─ 生成トークンをリクエストに追加
├─ 完了判定(EOS、max_tokens、stop_token)
│ → 完了時: kv_cache_manager.free(request) でブロック解放
└─ EngineCoreOutput 構築(request_id, new_token_ids, finish_reason, ...)
→ dict[int, EngineCoreOutputs](クライアントインデックス別)
下流パス: 実行 → ユーザー応答
実行層: Executor → Worker → GPUModelRunner
EngineCore.step()はexecutor.execute_model()を非ブロッキングで呼び出し、GPUでの推論実行を開始する。
参照: target/vllm/vllm/v1/executor/abstract.py:202 (execute_model)
collective_rpc パターン
Executorはcollective_rpc()パターンで全Workerに同一メソッドを実行させ、出力ランクのWorkerの結果のみを返す。
EngineCore.step()
│
├─ executor.execute_model(scheduler_output, non_block=True)
│ └─ collective_rpc("execute_model", args=(scheduler_output,))
│ └─ Worker.execute_model(scheduler_output) # L604
│ └─ model_runner.execute_model(scheduler_output) # L652
│ → ExecuteModelState を内部保存、None を返す
│
├─ grammar_output = scheduler.get_grammar_bitmask(...) # 並行処理
│
├─ future.result() → None # 待機
│
└─ executor.sample_tokens(grammar_output) # L222
└─ collective_rpc("sample_tokens", args=(grammar_output,))
└─ Worker.sample_tokens(grammar_output) # L598
└─ model_runner.sample_tokens(grammar_output) # L3621
→ ModelRunnerOutput を返す
参照: target/vllm/vllm/v1/worker/gpu_worker.py:604 (Worker.execute_model)
GPUModelRunner の2フェーズ実行
GPUModelRunnerは execute_model() と sample_tokens() を分離する2フェーズ実行パターンを採用する。これにより、モデルフォワード中にgrammar bitmask計算を並行実行できる。
Phase 1: execute_model() (target/vllm/vllm/v1/worker/gpu_model_runner.py:3312)
execute_model(scheduler_output)
├─ _update_states(scheduler_output) # バッチ状態更新
├─ _prepare_inputs(scheduler_output) # 入力ID・位置計算
├─ _build_attention_metadata(...) # Attention メタデータ構築
├─ _model_forward(...) # model.forward() 実行
│ → hidden_states
├─ compute_logits(hidden_states) # logits 計算
│ → logits
└─ ExecuteModelState に保存 → None を返す
Phase 2: sample_tokens() (target/vllm/vllm/v1/worker/gpu_model_runner.py:3621)
sample_tokens(grammar_output)
├─ ExecuteModelState を復元
├─ grammar bitmask 適用(構造化出力時)
├─ _sample(logits) → SamplerOutput
├─ バッチ状態更新(生成トークン反映)
└─ ModelRunnerOutput を構築して返す
ExecuteModelState (target/vllm/vllm/v1/worker/gpu_model_runner.py:313) はGPUテンソル(logits, hidden_states等)を保持するNamedTupleで、2フェーズ間の一時状態転送に使用される。
出力処理: EngineCoreOutput → RequestOutput
ModelRunnerOutputはバックエンドプロセス(EngineCore)でEngineCoreOutputに変換され、ZMQ経由でフロントエンドプロセスのOutputProcessorに送られてデトークナイズされる。
sequenceDiagram
participant MR as GPUModelRunner
participant S as Scheduler
participant EC as EngineCore
participant ZMQ as ZMQ IPC
participant OP as OutputProcessor
participant Client as API Client
MR->>EC: ModelRunnerOutput
EC->>S: update_from_output()
Note over S: トークン追加、完了判定<br>KVキャッシュ解放
S->>EC: dict[int, EngineCoreOutputs]
EC->>ZMQ: msgpack シリアライズ
ZMQ->>OP: EngineCoreOutputs
Note over OP: デトークナイズ<br>停止文字列判定<br>logprobs処理
OP->>Client: RequestOutput (yield)
OutputProcessor.process_outputs()
参照: target/vllm/vllm/v1/engine/output_processor.py:582 (process_outputs)
OutputProcessorはフロントエンドプロセスで動作し、EngineCoreOutputをユーザー向けRequestOutputに変換する。
OutputProcessor.process_outputs(engine_core_outputs) # L582
for each engine_core_output:
├─ req_state = request_states[req_id] # RequestState取得
│
├─ detokenizer.update(new_token_ids, stop_terminated) # L637
│ ├─ トークン→テキスト変換(インクリメンタル)
│ └─ 停止文字列チェック → stop_string or None
│
├─ logprobs_processor.update_from_output(output) # L646
│
├─ req_state.make_request_output(...) # L649
│ ├─ _new_completion_output(token_ids, finish_reason, ...)
│ │ ├─ detokenizer.get_next_output_text(finished, delta)
│ │ └─ CompletionOutput(text, token_ids, logprobs, ...)
│ └─ RequestOutput(request_id, outputs, finished, ...)
│
└─ req_state.queue.put(request_output) # L661
→ AsyncLLM.generate() が yield
Detokenizer(インクリメンタルデトークナイズ)
参照: target/vllm/vllm/v1/engine/detokenizer.py:30 (IncrementalDetokenizer)
トークンからテキストへの変換はインクリメンタルに行われ、ストリーミング出力を実現する。
| クラス | 条件 | 方式 |
|---|---|---|
FastIncrementalDetokenizer | PreTrainedTokenizerFast 使用時 | HF tokenizersのDecodeStreamで高速変換 |
SlowIncrementalDetokenizer | その他のトークナイザ | detokenize_incrementally()でPython変換 |
IncrementalDetokenizer | トークナイザなし | No-op(テキスト出力なし) |
update()メソッドで各トークンをインクリメンタルにデコードし、同時にcheck_stop_strings()で停止文字列を検出する(target/vllm/vllm/v1/engine/detokenizer.py:316)。
Prefill vs Decode
vLLM v1はUnified Compute Modelを採用し、PrefillとDecodeを明示的に区別しない。両者はnum_computed_tokensの進捗によって暗黙的に区分される。
統一管理の仕組み
各リクエストはnum_computed_tokensフィールドで計算済みトークン数を追跡する:
プロンプト: [A, B, C, D, E] (len=5)
num_computed_tokens: 0 → 5 → 6 → 7 → ...
Prefillフェーズ: num_computed_tokens < len(prompt_token_ids)
→ 複数トークンを一度に計算(チャンクプリフィル可能)
Decodeフェーズ: num_computed_tokens >= len(prompt_token_ids)
→ 1トークンずつ生成
Schedulerでの扱い
Scheduler.schedule()はPrefill/Decodeを区別せず、トークン予算の範囲内で各リクエストに計算トークン数を割り当てる:
- 新規リクエスト(WAITING→RUNNING):
num_tokens = len(prompt_token_ids) - num_computed_tokens(プレフィックスキャッシュヒット分を差し引き) - 継続リクエスト(RUNNING):
num_tokens = 1(Decode 1トークン) - 予算不足時は部分的なPrefill(チャンクプリフィル)も可能
GPUModelRunner内での違い
GPUModelRunner.execute_model()は入力準備の段階で暗黙的にPrefill/Decodeを処理する:
- _prepare_inputs():
num_scheduled_tokensに基づいて入力トークンと位置を計算。Prefillなら複数トークン、Decodeなら1トークン - _build_attention_metadata(): Prefillはフルattention、Decodeはキャッシュ済みKVに対するattentionのメタデータを構築
- モデルフォワード: 入力テンソルのサイズが異なるだけで、同一のforward()を実行
この統一モデルにより、同一バッチ内にPrefillリクエストとDecodeリクエストを混在させるContinuous Batchingが自然に実現される。
コンポーネント優先度(確定)
Phase 2での深堀り順序。ユーザー関心領域とフロー上の重要度に基づく。
| 優先度 | コンポーネント | 理由 | 現在の深度 |
|---|---|---|---|
| S | KVCacheManager | ユーザー関心1位(メモリ管理/KVキャッシュ)。PagedAttention、ブロック管理、Eviction | [MEDIUM] |
| A | Scheduler | KVCacheManagerと密連携、推論パイプライン全体を制御。Continuous Batching | [MEDIUM] |
| A | GPUModelRunner | 推論実行の中核。6277行の巨大クラス。将来のプラグイン開発に重要 | [SHALLOW] |
| B | EngineCore | step()サイクル、batch_queueパイプライン。全体の統合ポイント | [MEDIUM] |
| B | OutputProcessor | デトークナイズ、停止判定。ストリーミング出力の仕組み | [SHALLOW] |
| C | AsyncLLM, InputProcessor | エントリポイント。薄いレイヤー | [SHALLOW] |
| C | Executor, Worker | 委譲パターン。分散推論時のみ詳細が必要 | [SHALLOW] |
| C | EngineCoreClient | ZMQ IPC通信層。プロトコルは把握済み | [SHALLOW] |
参照ファイル一覧
| ファイル | 主要クラス/関数 | 役割 |
|---|---|---|
target/vllm/vllm/entrypoints/llm.py | LLM.generate() (L396), _add_request() (L1850) | 同期エントリポイント |
target/vllm/vllm/v1/engine/async_llm.py | AsyncLLM.generate() (L537), add_request() (L286) | 非同期エントリポイント |
target/vllm/vllm/v1/engine/input_processor.py | InputProcessor.process_inputs() (L521) | 入力処理 |
target/vllm/vllm/v1/engine/__init__.py | EngineCoreRequest (L55), EngineCoreOutput (L130), EngineCoreOutputs (L176) | 境界データ構造 |
target/vllm/vllm/v1/engine/core_client.py | EngineCoreClient (L63), MPClient (L442), AsyncMPClient (L822) | ZMQ IPC通信 |
target/vllm/vllm/v1/engine/core.py | EngineCore.add_request() (L288), step() (L389) | 推論ループ本体 |
target/vllm/vllm/v1/core/sched/scheduler.py | Scheduler.schedule() (L321), update_from_output() (L1241) | スケジューリング |
target/vllm/vllm/v1/core/sched/output.py | SchedulerOutput (L184), NewRequestData (L34), CachedRequestData (L114) | スケジュール出力データ構造 |
target/vllm/vllm/v1/core/kv_cache_manager.py | KVCacheManager.allocate_slots() (L206), get_computed_blocks() (L164) | KVキャッシュ管理 |
target/vllm/vllm/v1/core/block_pool.py | BlockPool (L128) | 物理ブロック管理 |
target/vllm/vllm/v1/request.py | Request | リクエスト内部状態 |
target/vllm/vllm/v1/outputs.py | ModelRunnerOutput (L160) | モデル推論出力 |
target/vllm/vllm/v1/executor/abstract.py | Executor (ABC), execute_model() (L202), collective_rpc() (L180) | 実行層抽象 |
target/vllm/vllm/v1/executor/uniproc_executor.py | UniProcExecutor (L26) | 単一プロセス実行 |
target/vllm/vllm/v1/executor/multiproc_executor.py | MultiprocExecutor (L93) | マルチプロセス実行 |
target/vllm/vllm/v1/worker/gpu_worker.py | Worker.execute_model() (L604), sample_tokens() (L598) | GPU Worker |
target/vllm/vllm/v1/worker/gpu_model_runner.py | GPUModelRunner.execute_model() (L3312), sample_tokens() (L3621), ExecuteModelState (L313) | モデル実行 |
target/vllm/vllm/v1/engine/output_processor.py | OutputProcessor.process_outputs() (L582), RequestState.make_request_output() (L269) | 出力処理 |
target/vllm/vllm/v1/engine/detokenizer.py | IncrementalDetokenizer (L30), FastIncrementalDetokenizer (L169), check_stop_strings() (L316) | デトークナイズ |
target/vllm/vllm/v1/engine/logprobs.py | LogprobsProcessor (L28) | logprobs処理 |
target/vllm/vllm/outputs.py | RequestOutput (L86), CompletionOutput (L23) | 最終出力データ構造 |
マルチモーダル推論パスの差分
テキスト推論フローに対し、画像等のマルチモーダル入力がある場合の主要な差分を以下に示す。詳細は マルチモーダル処理パイプライン を参照。
フロントエンド(P0)の差分
- チャットテンプレート: プレースホルダー(
<start_of_image>等)がプロンプトに挿入される - HF Processor実行: 画像を
pixel_valuesテンソルに変換(リサイズ、正規化、パッチ分割) - MMハッシュ計算:
MultiModalHasherでコンテンツベースのblake3ハッシュを生成 - ProcessorCache: HF処理結果をキャッシュ(4種類の実装: processor_only/lru/shm/none)
- EngineCoreRequest:
mm_features: list[MultiModalFeatureSpec]にテンソルデータ・位置情報・ハッシュを格納
バックエンド(P1)の差分
- EncoderCacheManager: エンコーダ出力をリファレンスカウント方式で管理。キャッシュヒットでエンコーダ計算スキップ
- Scheduler:
encoder_compute_budgetでステップあたりのエンコーダ計算量を制御 - GPUModelRunner:
_execute_mm_encoder(): ビジョンエンコーダ実行(model.embed_multimodal())_gather_mm_embeddings(): キャッシュからプレースホルダー位置に対応する埋め込みを取得embed_input_ids():masked_scatter_でテキスト埋め込みとビジョン埋め込みをマージ
- モデルforward:
input_idsではなくinputs_embeds(マージ済み)が渡される
アーキテクチャ概要
深度: [SHALLOW] 確信度: [VERIFIED] 最終更新: 2026-02-09
概要
vLLMはUC Berkeley Sky Computing Lab発のLLM推論・サービングライブラリである。PagedAttentionによるKVキャッシュの効率的メモリ管理、Continuous Batchingによる動的バッチスケジューリングを中核技術とし、高スループット・低レイテンシのLLM推論を実現する。OpenAI互換APIサーバー、マルチモーダル対応、分散推論(Tensor/Pipeline/Data/Expert並列)を備える。
全体構造
graph TD
subgraph エントリポイント層
CLI["CLI<br>vllm.entrypoints.cli"]
LLM["LLM<br>vllm.entrypoints.llm:101"]
OpenAI["OpenAI互換API<br>vllm.entrypoints.openai"]
end
subgraph エンジン層
AsyncLLM["AsyncLLM<br>vllm.v1.engine.async_llm:71"]
LLMEngine["LLMEngine<br>vllm.v1.engine.llm_engine"]
EngineCore["EngineCore<br>vllm.v1.engine.core:79"]
InputProc["InputProcessor"]
OutputProc["OutputProcessor"]
end
subgraph コア層
Scheduler["Scheduler<br>vllm.v1.core.sched.scheduler:63"]
KVCacheMgr["KVCacheManager<br>vllm.v1.core.kv_cache_manager:94"]
BlockPool["BlockPool<br>vllm.v1.core.block_pool"]
end
subgraph 実行層
Executor["Executor<br>vllm.v1.executor"]
Worker["Worker<br>vllm.v1.worker.gpu_worker:70"]
ModelRunner["GPUModelRunner<br>vllm.v1.worker.gpu_model_runner:329"]
end
subgraph モデル層
Models["Models<br>vllm.model_executor.models<br>241ファイル"]
Attention["Attention<br>2層構造"]
Layers["Layers<br>vllm.model_executor.layers"]
end
CLI --> AsyncLLM
LLM --> LLMEngine
OpenAI --> AsyncLLM
AsyncLLM -->|"ZMQ IPC"| EngineCore
LLMEngine --> EngineCore
EngineCore --> InputProc
EngineCore --> OutputProc
EngineCore --> Scheduler
EngineCore --> KVCacheMgr
KVCacheMgr --> BlockPool
EngineCore --> Executor
Executor --> Worker
Worker --> ModelRunner
ModelRunner --> Models
ModelRunner --> Attention
Models --> Layers
アーキテクチャの世代
vllm/engine/ は vllm/v1/ への薄いラッパーである。
参照: target/vllm/vllm/engine/llm_engine.py:4 — LLMEngine = V1LLMEngine の1行エイリアス
v1が現行アーキテクチャの本体であり、コードリーディングでは vllm/v1/ を中心に読む。ただし vllm/model_executor/、vllm/distributed/、vllm/multimodal/ 等はv1からも直接利用されるため調査対象に含む。
主要コンポーネント
| コンポーネント | クラス | パス | 役割 |
|---|---|---|---|
| AsyncLLM | AsyncLLM(EngineClient) | target/vllm/vllm/v1/engine/async_llm.py:71 | 非同期APIトップレベル |
| EngineCore | EngineCore | target/vllm/vllm/v1/engine/core.py:79 | 推論ループ内側。ZMQで外側と通信 |
| Scheduler | Scheduler(SchedulerInterface) | target/vllm/vllm/v1/core/sched/scheduler.py:63 | Continuous Batchingスケジューラ |
| KVCacheManager | KVCacheManager | target/vllm/vllm/v1/core/kv_cache_manager.py:94 | KVキャッシュブロック管理 |
| Executor | Executor(ABC) | target/vllm/vllm/v1/executor/abstract.py | Worker群を束ねる実行層 |
| Worker | Worker(WorkerBase) | target/vllm/vllm/v1/worker/gpu_worker.py:70 | 1 GPUデバイスを担当 |
| GPUModelRunner | GPUModelRunner | target/vllm/vllm/v1/worker/gpu_model_runner.py:329 | GPU上のフォワードパス実行 |
| VllmConfig | VllmConfig | target/vllm/vllm/config/vllm.py | 全設定の集約クラス |
設計原則
PagedAttention
KVキャッシュをOSの仮想メモリページングに着想を得てブロック単位で管理する。連続したGPUメモリ確保が不要になり、メモリ断片化を大幅に抑制する。
Continuous Batching
リクエストの到着・完了に応じてバッチを動的に更新する。固定バッチサイズと異なり、GPU稼働率を最大化できる。
ZMQ IPC によるプロセス分離
EngineCoreは別プロセス(EngineCoreProc)として動作し、ZeroMQソケットで上位エンジン層と通信する。これによりスケジューリングと推論処理を並行実行できる。
プラグインシステム
vllm/plugins/ によるプラグイン機構を備え、起動時に load_general_plugins() で拡張を読み込む。
C++/CUDA 拡張
target/vllm/csrc/ にパフォーマンスクリティカルなネイティブコードが配置されている。PagedAttentionカーネル、LayerNorm、量子化カーネル、カスタムAllReduce等が含まれる。Pythonからは vllm._custom_ops 等のバインディング経由で呼び出される。
参照ファイル
| ファイル | 主要クラス/関数 |
|---|---|
target/vllm/vllm/engine/llm_engine.py | LLMEngine(v1への薄いラッパー) |
target/vllm/vllm/v1/engine/async_llm.py | AsyncLLM |
target/vllm/vllm/v1/engine/core.py | EngineCore, EngineCoreProc |
target/vllm/vllm/v1/core/sched/scheduler.py | Scheduler |
target/vllm/vllm/v1/core/kv_cache_manager.py | KVCacheManager |
target/vllm/vllm/v1/executor/abstract.py | Executor(ABC) |
target/vllm/vllm/v1/worker/gpu_worker.py | Worker |
target/vllm/vllm/v1/worker/gpu_model_runner.py | GPUModelRunner |
target/vllm/vllm/config/vllm.py | VllmConfig |
target/vllm/vllm/entrypoints/llm.py | LLM |
ECConnector(Encoder Cache Connector)
深度: [MEDIUM] | 確信度: [VERIFIED] | 最終更新: 2026-02-14
概要
ECConnectorは、マルチモーダルモデルのエンコーダ出力をvLLMインスタンス間または外部ストレージと転送するためのプラグインフレームワークである。KV Transfer(デコーダKVキャッシュ用)とは完全に独立した系統で、エンコーダキャッシュに特化している。
主なユースケースは**Encoder-Prefill-Decode分離(EPD)**で、エンコーダ専用インスタンスが画像処理を行い、その結果をデコーダインスタンスに転送する。
参照: target/vllm/vllm/distributed/ec_transfer/ (パッケージ全体)
アーキテクチャ
ファイル構成
vllm/distributed/ec_transfer/
├── __init__.py # get_ec_transfer(), has_ec_transfer() 公開API
├── ec_transfer_state.py # グローバルシングルトン管理
└── ec_connector/
├── __init__.py
├── base.py # ECConnectorBase 抽象基底クラス
├── factory.py # ECConnectorFactory レジストリ + 動的ロード
└── example_connector.py # ECExampleConnector 参照実装(safetensors)
vllm/v1/worker/
└── ec_connector_model_runner_mixin.py # GPUModelRunner統合Mixin
vllm/config/
└── ec_transfer.py # ECTransferConfig 設定クラス
2ロール分離アーキテクチャ
ECConnectorはScheduler側とWorker側に分離され、同じクラスが両方のロールを担う:
graph TB
subgraph "Scheduler Process (ECConnectorRole.SCHEDULER)"
SC[ECConnector Scheduler側]
SCH[Scheduler]
SCH -->|has_cache_item| SC
SCH -->|update_state_after_alloc| SC
SCH -->|build_connector_meta| SC
end
subgraph "Worker Process (ECConnectorRole.WORKER)"
WC[ECConnector Worker側]
GMR[GPUModelRunner]
GMR -->|bind_connector_metadata| WC
GMR -->|start_load_caches| WC
GMR -->|save_caches| WC
GMR -->|get_finished| WC
end
SC -->|ECConnectorMetadata<br/>SchedulerOutput経由| WC
| ロール | 生成場所 | 主な責務 |
|---|---|---|
| SCHEDULER | Scheduler.__init__() via ECConnectorFactory | キャッシュ存在チェック、メタデータ構築 |
| WORKER | gpu_worker.py の ensure_ec_transfer_initialized() | キャッシュのロード/セーブ |
参照: target/vllm/vllm/v1/core/sched/scheduler.py:135-138 (Scheduler側生成)
参照: target/vllm/vllm/distributed/ec_transfer/ec_transfer_state.py:26-43 (Worker側生成)
ECConnectorBase 抽象基底クラス
参照: target/vllm/vllm/distributed/ec_transfer/ec_connector/base.py:59-253
プロパティ
| プロパティ | 型 | 説明 |
|---|---|---|
role | ECConnectorRole | SCHEDULER or WORKER |
is_producer | bool | エンコーダキャッシュを生成する側か |
is_consumer | bool | エンコーダキャッシュを消費する側か |
抽象メソッド(実装必須)
Worker側(3メソッド)
| メソッド | シグネチャ | 説明 |
|---|---|---|
start_load_caches | (encoder_cache: dict[str, Tensor], **kwargs) → None | メタデータに基づきキャッシュをロード |
save_caches | (encoder_cache: dict[str, Tensor], mm_hash: str, **kwargs) → None | エンコーダ出力を外部に保存 |
Scheduler側(3メソッド)
| メソッド | シグネチャ | 説明 |
|---|---|---|
has_cache_item | (identifier: str) → bool | 外部にキャッシュが存在するか判定 |
update_state_after_alloc | (request: Request, index: int) → None | 割当後の内部状態更新 |
build_connector_meta | (scheduler_output: SchedulerOutput) → ECConnectorMetadata | Worker転送用メタデータ構築 |
具象メソッド(オーバーライド任意)
| メソッド | デフォルト動作 | 説明 |
|---|---|---|
bind_connector_metadata | メタデータ保持 | Worker側: 毎step実行前に呼ばれる |
clear_connector_metadata | Noneに設定 | Worker側: 毎step実行後に呼ばれる |
register_caches | no-op | 将来のP2P機能用 |
get_finished | (None, None) | 非同期転送完了通知 |
update_connector_output | no-op | Worker出力からScheduler状態を更新 |
request_finished | (False, None) | リクエスト完了時のフック |
ECTransferConfig 設定
参照: target/vllm/vllm/config/ec_transfer.py:16-108
ECロール
ECRole = Literal["ec_producer", "ec_consumer", "ec_both"]
| ロール | 説明 | is_producer | is_consumer |
|---|---|---|---|
ec_producer | エンコーダ計算+キャッシュ保存 | True | False |
ec_consumer | キャッシュ読み込み+デコーダ実行 | False | True |
ec_both | 両方の機能 | True | True |
主要設定パラメータ
| パラメータ | デフォルト | 説明 |
|---|---|---|
ec_connector | None | コネクタ名(例: “ECExampleConnector”) |
ec_role | None | ECロール |
ec_connector_module_path | None | カスタムコネクタのPythonモジュールパス |
ec_connector_extra_config | {} | コネクタ固有の追加設定 |
ec_buffer_device | “cuda” | バッファデバイス |
ec_buffer_size | 1e9 | バッファサイズ(バイト) |
ec_ip / ec_port | 127.0.0.1:14579 | P2P接続用 |
ec_rank / ec_parallel_size | None / 1 | 分散接続設定 |
ECConnectorFactory
参照: target/vllm/vllm/distributed/ec_transfer/ec_connector/factory.py:20-85
コネクタ登録方式
2つの登録方法がある:
- 静的登録:
ECConnectorFactory.register_connector()でモジュール遅延ロード登録 - 動的ロード:
ec_connector_module_pathで任意のPythonモジュールからロード
# 静的登録(factory.py末尾)
ECConnectorFactory.register_connector(
"ECExampleConnector",
"vllm.distributed.ec_transfer.ec_connector.example_connector",
"ECExampleConnector",
)
# 動的ロード(ec_connector_module_pathが設定されている場合)
connector_module = importlib.import_module(connector_module_path)
connector_cls = getattr(connector_module, connector_name)
現在登録済みコネクタ
| 名前 | 実装 | 用途 |
|---|---|---|
ECExampleConnector | example_connector.py | 参照実装(safetensorsディスク保存) |
ECExampleConnector 参照実装
参照: target/vllm/vllm/distributed/ec_transfer/ec_connector/example_connector.py:45-199
safetensorsフォーマットでディスクにエンコーダキャッシュを保存/読み込みする参照実装。
メタデータ
@dataclass
class MMMeta:
mm_hash: str # マルチモーダルデータのハッシュ
num_token: int # エンコーダトークン数
@dataclass
class ECExampleConnectorMetadata(ECConnectorMetadata):
mm_datas: list[MMMeta] # ロードすべきエントリ一覧
ストレージ構造
{shared_storage_path}/
└── {mm_hash}/
└── encoder_cache.safetensors # {"ec_cache": Tensor} 形式
動作フロー
保存(Producer側)
参照: target/vllm/vllm/distributed/ec_transfer/ec_connector/example_connector.py:98-118
- GPUModelRunnerが
_execute_mm_encoder()完了後にmaybe_save_ec_to_connector()を呼ぶ save_caches(): テンソルを.detach().cpu()してsafetensorsで保存
読み込み(Consumer側)
参照: target/vllm/vllm/distributed/ec_transfer/ec_connector/example_connector.py:63-96
- Scheduler側:
has_cache_item()でファイル存在確認 (os.path.exists) - Scheduler側:
build_connector_meta()でロード対象リストを構築 - Worker側:
start_load_caches()でsafetensorsからGPUにロード
存在確認
has_cache_item() は os.path.exists() でsafetensorsファイルの存在をチェック。
Schedulerとの統合
_schedule_encoder_inputs() 内の分岐
参照: target/vllm/vllm/v1/core/sched/scheduler.py:1212-1218
if self.ec_connector is not None and self.ec_connector.has_cache_item(identifier):
# 外部キャッシュにヒット → エンコーダ計算不要、compute_budget消費なし
mm_hashes_to_schedule.add(item_identifier)
external_load_encoder_input.append(i)
num_embeds_to_schedule += num_encoder_embeds
continue
ECConnectorにキャッシュがある場合:
encoder_compute_budgetは消費しない(エンコーダ計算不要のため)external_load_encoder_inputリストに追加encoder_cache_manager.allocate()は実行される(GPU側に空きが必要)
割当後の状態更新
参照: target/vllm/vllm/v1/core/sched/scheduler.py:523-527
if external_load_encoder_input:
for i in external_load_encoder_input:
self.encoder_cache_manager.allocate(request, i)
if self.ec_connector is not None:
self.ec_connector.update_state_after_alloc(request, i)
メタデータ構築
参照: target/vllm/vllm/v1/core/sched/scheduler.py:899-904
build_connector_meta() がSchedulerOutputに ec_connector_metadata を設定。Worker側はこのメタデータを使ってロード対象を特定する。
GPUModelRunnerとの統合
ECConnectorModelRunnerMixin
参照: target/vllm/vllm/v1/worker/ec_connector_model_runner_mixin.py:25-87
GPUModelRunnerに3つのstatic methodを提供:
| メソッド | 説明 |
|---|---|
maybe_save_ec_to_connector | エンコーダ出力保存(Producer時) |
get_finished_ec_transfers | 非同期転送完了確認 |
maybe_get_ec_connector_output | コンテキストマネージャでライフサイクル管理 |
コンテキストマネージャのライフサイクル
with self.maybe_get_ec_connector_output(scheduler_output, encoder_cache) as output:
# 1. bind_connector_metadata() → メタデータ設定
# 2. Consumer時: start_load_caches() → 外部からロード
# 3. yield → エンコーダ実行、gather処理
# 4. get_finished() → 非同期完了確認
# 5. clear_connector_metadata() → クリーンアップ
Producer専用モード
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:3343-3349
Producer専用インスタンスは、エンコーダ実行後にデコーダ実行をスキップし、空のModelRunnerOutputを返す:
if has_ec_transfer() and get_ec_transfer().is_producer:
with self.maybe_get_ec_connector_output(...) as ec_connector_output:
self._execute_mm_encoder(scheduler_output)
return make_empty_encoder_model_runner_output(scheduler_output)
また、Producer専用インスタンスはKVキャッシュを確保しない:
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:6160-6161
if has_ec_transfer() and get_ec_transfer().is_producer:
return {} # KVCacheSpec空 → KVキャッシュ確保なし
ECConnectorOutput
参照: target/vllm/vllm/v1/outputs.py:151-154
@dataclass
class ECConnectorOutput:
finished_sending: set[str] | None = None
finished_recving: set[str] | None = None
ModelRunnerOutputに含まれてScheduler側に返されるが、現時点ではScheduler側で未消費(ec_connector_outputを読み取るコードがSchedulerにない)。非同期転送完了フィードバックは将来実装予定。
グローバルシングルトン管理
参照: target/vllm/vllm/distributed/ec_transfer/ec_transfer_state.py:14-43
_EC_CONNECTOR_AGENT: ECConnectorBase | None = None
| 関数 | 説明 |
|---|---|
has_ec_transfer() | ECConnectorが初期化済みか |
get_ec_transfer() | シングルトン取得(未初期化ならassert) |
ensure_ec_transfer_initialized(config) | Workerロールで初期化(冪等) |
Worker側のシングルトン初期化は gpu_worker.py で ensure_ec_transfer_initialized() を呼ぶことで行われる。Scheduler側は ECConnectorFactory.create_connector() で直接生成し、self.ec_connector に保持する(シングルトンではない)。
カスタムECConnector実装ガイド
最小実装
ECConnectorBaseを継承- 5つの抽象メソッドを実装
- ECTransferConfigの
ec_connectorにクラス名、ec_connector_module_pathにモジュールパスを指定
実装の要点
has_cache_item()はSchedulerのホットパスで呼ばれるため高速であるべきstart_load_caches()はencoder_cachedict に直接テンソルを追加するsave_caches()はencoder_cache[mm_hash]からGPUテンソルを取得して保存するbuild_connector_meta()は内部状態をリセットすること
起動コマンド例
# Producer(エンコーダ専用インスタンス)
vllm serve model_name \
--ec-connector ECExampleConnector \
--ec-role ec_producer \
--ec-connector-extra-config '{"shared_storage_path": "/shared/cache"}'
# Consumer(デコーダインスタンス)
vllm serve model_name \
--ec-connector ECExampleConnector \
--ec-role ec_consumer \
--ec-connector-extra-config '{"shared_storage_path": "/shared/cache"}'
上流・下流依存関係
上流
- ECTransferConfig:
ec_connector,ec_role等の設定 - Scheduler: キャッシュ存在確認、状態更新、メタデータ構築の呼び出し
- GPUModelRunner: エンコーダ実行結果の保存、ロード済みキャッシュの利用
下流
- 外部ストレージ: safetensors(例)、共有メモリ、ネットワーク等(実装依存)
開発状況・未実装機能
- ECConnectorOutput未消費: Worker→Scheduler方向の非同期転送完了フィードバックが未実装
- request_finished未統合: Schedulerから
ec_connector.request_finished()が呼ばれていない - register_caches未実装: P2P直接転送のためのキャッシュ登録(TODO)
- エンコーダキャッシュ事前割り当て未対応:
encoder_cacheがdictのため、固定バッファへの移行が必要 - 登録済みコネクタが1つのみ: ECExampleConnector(デバッグ用)のみ。SHMConnector等は外部PR待ち
EncoderCache(エンコーダキャッシュ)
深度: [MEDIUM] | 確信度: [VERIFIED] | 最終更新: 2026-02-14
概要
EncoderCacheは、マルチモーダルモデルにおけるエンコーダ出力(例: ビジョンエンコーダの画像埋め込み)のGPUメモリ上キャッシュを管理するコンポーネントである。2層構造で、Scheduler側の論理管理(EncoderCacheManager)とWorker側の物理ストレージ(GPUModelRunner.encoder_cache)に分離されている。
参照: target/vllm/vllm/v1/core/encoder_cache_manager.py (EncoderCacheManager)
アーキテクチャ
2層構造
graph TB
subgraph Scheduler Process
ECM[EncoderCacheManager<br/>論理管理]
end
subgraph Worker Process
EC["encoder_cache: dict[str, Tensor]<br/>GPU物理ストレージ"]
end
ECM -->|"free_encoder_mm_hashes<br/>(SchedulerOutput経由)"| EC
ECM -->|"scheduled_encoder_inputs<br/>(何を計算すべきか)"| EC
| 層 | 場所 | データ構造 | 役割 |
|---|---|---|---|
| 論理管理 | Scheduler | EncoderCacheManager | キャッシュ容量管理、参照カウント、Eviction判定 |
| 物理ストレージ | GPUModelRunner | dict[str, torch.Tensor] | mm_hash → エンコーダ出力テンソルの保持 |
EncoderCacheManager 詳細
主要フィールド
参照: target/vllm/vllm/v1/core/encoder_cache_manager.py:67-77
| フィールド | 型 | 説明 |
|---|---|---|
cache_size | int | エンコーダ埋め込み数で測った総容量 |
num_free_slots | int | 現在利用可能な空きスロット数 |
num_freeable_slots | int | 参照ゼロエントリの回収で即座に利用可能になるスロット数 |
cached | dict[str, set[str]] | mm_hash → 参照中リクエストIDの集合 |
freeable | OrderedDict[str, int] | 参照ゼロエントリの挿入順リスト(mm_hash → 埋め込み数) |
freed | list[str] | 直近のEvictionで物理解放すべきmm_hashリスト |
Eviction方式: FIFO(参照ゼロエントリの遅延解放)
EncoderCacheManagerは遅延解放FIFO方式を採用する:
- リクエスト完了時、参照カウントが0になったエントリは即座には解放されず
freeableOrderedDictに追加 - 新しいエンコーダ出力のキャッシュ確保(
can_allocate())時に空きが不足した場合のみ、古い順にEviction - Evictionされたmm_hashは
freedリストに追加され、次のget_freed_mm_hashes()でWorkerに通知 - Worker側で
encoder_cache.pop(mm_hash)により物理メモリ解放
参照: target/vllm/vllm/v1/core/encoder_cache_manager.py:119-178 (can_allocate)
stateDiagram-v2
[*] --> Active: allocate()
Active --> Active: check_and_update_cache()で新リクエスト参照追加
Active --> Freeable: free_encoder_input()で参照カウント=0
Freeable --> Active: check_and_update_cache()で再参照
Freeable --> Evicted: can_allocate()で空き不足時
Evicted --> [*]: Worker側でpop()
共有キャッシュ
同じ画像を含む複数リクエストが同時に処理される場合、同一のmm_hashを持つエンコーダ出力は共有される。cached dictのvalue(set[str])が複数リクエストIDを保持し、全リクエストの完了後にのみfreeableに移行する。
参照: target/vllm/vllm/v1/core/encoder_cache_manager.py:91-117 (check_and_update_cache)
主要メソッド
| メソッド | 呼び出し元 | 説明 |
|---|---|---|
check_and_update_cache(request, input_id) | Scheduler._schedule_encoder_inputs | キャッシュヒット判定。ヒット時は参照追加してTrue |
can_allocate(request, input_id, budget, scheduled) | Scheduler._schedule_encoder_inputs | 空き確認+必要時Eviction。False=予算不足 |
allocate(request, input_id) | Scheduler(RUNNING/WAITING処理) | 論理的にキャッシュ空間を確保 |
free_encoder_input(request, input_id) | Scheduler | 1エントリの参照解放 |
free(request) | Scheduler(リクエスト完了/中断時) | 全エントリの参照解放 |
get_freed_mm_hashes() | Scheduler._build_scheduler_output | Eviction済みmm_hashリスト取得(Worker通知用) |
キャッシュ容量の決定
参照: target/vllm/vllm/v1/core/encoder_cache_manager.py:269-316 (compute_mm_encoder_budget)
encoder_compute_budget = max(max_num_encoder_input_tokens, max_tokens_per_mm_item)
encoder_cache_size = max(encoder_cache_size_config, max_tokens_per_mm_item)
max_tokens_per_mm_item: モデルがサポートする全モダリティの最大トークン数max_num_encoder_input_tokens: SchedulerConfig設定値- 1アイテムは必ずキャッシュできることを保証
GPUModelRunner.encoder_cache(物理ストレージ)
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:439
self.encoder_cache: dict[str, torch.Tensor] = {}
- キー:
mm_hash(マルチモーダルデータのidentifier) - 値: エンコーダ出力テンソル(GPU上)
- 書き込み:
_execute_mm_encoder()完了時にencoder_cache[mm_hash] = output - 読み取り:
_gather_mm_embeddings()でデコーダ入力に合成 - 削除:
_update_states()でscheduler_output.free_encoder_mm_hashesに従いpop()
ECConnectorとの連携
エンコーダ出力をGPUキャッシュに保存した直後に、ECConnector(有効時)にも保存する:
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:2442-2445
for mm_hash, output in zip(mm_hashes, encoder_outputs):
self.encoder_cache[mm_hash] = output
self.maybe_save_ec_to_connector(self.encoder_cache, mm_hash)
Consumer側では、execute_model() の冒頭で ECConnector から encoder_cache にロードする:
参照: target/vllm/vllm/v1/worker/ec_connector_model_runner_mixin.py:76-77
if ec_connector.is_consumer:
ec_connector.start_load_caches(encoder_cache, **kwargs)
EncoderDecoderCacheManager
参照: target/vllm/vllm/v1/core/encoder_cache_manager.py:323-382
Encoder-Decoderモデル(例: Whisper)用の暫定実装。EncoderCacheManagerを継承するがキャッシュ共有機能を持たず、毎回エンコーダを実行する。主な違い:
| 特性 | EncoderCacheManager (MM) | EncoderDecoderCacheManager |
|---|---|---|
| キャッシュ共有 | あり(mm_hash based) | なし(常にFalse) |
| 参照カウント | あり | なし |
| Eviction | FIFO遅延解放 | 即時解放(1step遅延バッファあり) |
| 用途 | Vision-Language Model | Encoder-Decoder Model |
上流・下流依存関係
上流
- Scheduler:
_schedule_encoder_inputs()でキャッシュヒット判定・割当・解放指示 - SchedulerConfig:
encoder_cache_size,max_num_encoder_input_tokensで容量決定
下流
- GPUModelRunner: 物理テンソル保持、エンコーダ実行、gather処理
- ECConnector: 外部ストレージへの保存/読み込み(有効時)
データフロー
sequenceDiagram
participant S as Scheduler
participant ECM as EncoderCacheManager
participant SO as SchedulerOutput
participant GMR as GPUModelRunner
participant EC as encoder_cache (GPU)
Note over S: _schedule_encoder_inputs()
S->>ECM: check_and_update_cache(req, i)
alt キャッシュヒット
ECM-->>S: True(計算スキップ)
else キャッシュミス
ECM-->>S: False
S->>ECM: can_allocate(req, i, budget, scheduled)
alt 空きあり(Eviction含む)
ECM-->>S: True
S->>ECM: allocate(req, i)
else 空き不足
ECM-->>S: False(トークン数調整)
end
end
Note over S: _build_scheduler_output()
S->>ECM: get_freed_mm_hashes()
ECM-->>S: freed list
S->>SO: free_encoder_mm_hashes, scheduled_encoder_inputs
Note over GMR: execute_model()
GMR->>EC: _execute_mm_encoder() → 保存
GMR->>EC: _gather_mm_embeddings() → 読取
GMR->>EC: _update_states() → free_encoder_mm_hashes に従い削除
注意事項
- キャッシュサイズはエンコーダ埋め込み数で測定される。画像間のテキストトークン(break tokens等)は含まない
- 物理メモリ解放はEviction判定(Scheduler側)と実際の
pop()(Worker側)の間に1step以上のラグがある num_freeable_slotsはnum_free_slots以上の値を常に持つ(freeable + free の合計)
EngineCore サマリー
深度: [MEDIUM] 確信度: [VERIFIED] 最終更新: 2026-02-11
概要
EngineCoreはバックエンドプロセス(EngineCoreProc)内で動作する推論ループの中央制御コンポーネントである。Scheduler、ModelExecutor、KVCacheManagerを統括し、step()メソッドで schedule → execute → update のサイクルを繰り返す。フロントエンドプロセスとはZMQ IPC経由で通信し、EngineCoreRequestを受信してEngineCoreOutputsを返す。
アーキテクチャ
graph TD
subgraph EngineCoreプロセス
EC["EngineCore"]
Sched["Scheduler"]
KVM["KVCacheManager"]
Exec["ModelExecutor"]
EC -->|"1. schedule()"| Sched
Sched -->|"allocate_slots()"| KVM
Sched -->|"SchedulerOutput"| EC
EC -->|"2. execute_model()"| Exec
Exec -->|"Future<ModelRunnerOutput>"| EC
EC -->|"3. update_from_output()"| Sched
Sched -->|"EngineCoreOutputs"| EC
end
ZMQ_IN["ZMQ ROUTER<br>(受信)"] -->|"EngineCoreRequest"| EC
EC -->|"EngineCoreOutputs"| ZMQ_OUT["ZMQ PUSH<br>(送信)"]
主要コンポーネント
| コンポーネント | 用途 | ファイル |
|---|---|---|
EngineCore | 推論ループ本体 | target/vllm/vllm/v1/engine/core.py:82 |
EngineCoreProc | プロセスラッパー。ZMQソケット管理とイベントループ | target/vllm/vllm/v1/engine/core.py |
主要メソッド
| メソッド | 行 | 説明 |
|---|---|---|
__init__() | L82 | Scheduler, ModelExecutor, KVキャッシュの初期化 |
step() | L389 | メインループ: schedule → execute → update |
step_with_batch_queue() | L434 | パイプライン並列化版step(batch_queue使用) |
add_request() | L288 | リクエストをバリデーション後Schedulerに登録 |
post_step() | L424 | step後処理(Speculative Decodingのドラフトトークン更新) |
step() サイクル
参照: target/vllm/vllm/v1/engine/core.py:389
EngineCore.step() -> tuple[dict[int, EngineCoreOutputs], bool]
│
├─ スケジューラ停止チェック # L397
│ if _scheduler_paused: return {}, False
│
├─ リクエスト有無チェック # L402
│ if not scheduler.has_requests(): return {}, False
│
├─ 1. scheduler.schedule() # L404
│ → SchedulerOutput
│
├─ 2. executor.execute_model(scheduler_output, # L405
│ non_block=True)
│ → Future[ModelRunnerOutput | None]
│
├─ 3. scheduler.get_grammar_bitmask(scheduler_output) # L406
│ → grammar_output(構造化出力用)
│
├─ 4. future.result() # L411
│ → ModelRunnerOutput(ブロッキング待機)
│
├─ 5. if model_output is None: # L413
│ model_output = executor.sample_tokens(grammar_output)
│ (非同期スケジューリング時: execute_modelとsamplingが分離)
│
├─ 6. _process_aborts_queue() # L417
│
└─ 7. scheduler.update_from_output( # L418
│ scheduler_output, model_output)
│ → dict[int, EngineCoreOutputs]
│
└─ return (engine_core_outputs, # L422
total_num_scheduled_tokens > 0)
戻り値:
- 第1要素: クライアントインデックス → EngineCoreOutputs のマッピング
- 第2要素: モデル実行が行われたか(
total_num_scheduled_tokens > 0)
add_request() フロー
参照: target/vllm/vllm/v1/engine/core.py:288
add_request(request, request_wave=0) # L288
├─ request_id の型チェック(str必須) # L295
├─ pooling_params のタスクバリデーション # L300
├─ kv_transfer_params の互換性チェック # L311
└─ scheduler.add_request(request) # L319
batch_queue パイプライン並列化 [SHALLOW]
参照: target/vllm/vllm/v1/engine/core.py:434 (step_with_batch_queue)
max_concurrent_batches > 1 の場合、step_with_batch_queue() が step_fn として使用される。スケジューリングとモデル実行をパイプライン的にオーバーラップさせ、GPUのアイドル時間を削減する。
batch_queue:deque[tuple[Future, SchedulerOutput, Future]]- 新しいスケジュール結果を
appendleft()で追加、完了待ちをpop()で取得 - 前のバッチの実行完了を待たずに次のバッチをスケジュール可能
KVキャッシュ初期化フロー [SHALLOW]
参照: target/vllm/vllm/v1/engine/core.py:82 (init)
EngineCore.__init__()
→ _initialize_kv_caches()
→ model_executor.get_kv_cache_specs() # モデルのKVキャッシュ要件取得
→ determine_available_memory() # GPUメモリプロファイリング
→ get_kv_cache_configs() # ブロック数等の設定算出
→ generate_scheduler_kv_cache_config() # Scheduler用設定生成
→ model_executor.initialize_from_config() # GPUメモリ確保
KV Connector(KV Transfer/LMCache連携)が有効な場合:
scheduler.get_kv_connector()でコネクタ有無を確認 (L159)- 各ワーカーのハンドシェイクメタデータを収集・統合 (L164-175)
async_scheduling [SHALLOW]
vllm_config.scheduler_config.async_scheduling で有効化。
- 通常:
execute_model()がモデル実行 + トークンサンプリングをまとめて実行 - async有効時:
execute_model()はモデル実行のみ(Noneを返す)→sample_tokens()で別途サンプリング post_step()でのSpeculative Decodingドラフトトークン更新タイミングに影響
設定
| パラメータ | デフォルト | 説明 |
|---|---|---|
max_concurrent_batches | 1 | batch_queueサイズ(>1でパイプライン並列化) |
async_scheduling | False | 非同期スケジューリングモード |
呼び出しフロー
[EngineCoreProc イベントループ]
├─ ZMQ受信 → EngineCore.add_request()
├─ EngineCore.step_fn() (= step() or step_with_batch_queue())
│ ├─ Scheduler.schedule()
│ ├─ ModelExecutor.execute_model()
│ └─ Scheduler.update_from_output()
├─ EngineCore.post_step()
└─ ZMQ送信 ← EngineCoreOutputs
関連ドキュメント
EngineCoreClient サマリー
深度: [SHALLOW] 確信度: [VERIFIED] 最終更新: 2026-02-09
概要
EngineCoreClientはフロントエンドプロセス(AsyncLLM / LLM)とバックエンドプロセス(EngineCore)間のプロセス間通信を抽象化するコンポーネントである。ZeroMQソケットとmsgpackシリアライゼーションを使用し、EngineCoreRequestの送信とEngineCoreOutputsの受信を効率的に行う。
アーキテクチャ
フロントエンドプロセス バックエンドプロセス
┌───────────────────┐ ┌───────────────────┐
│ AsyncMPClient │ │ EngineCore │
│ │ │ │
│ input_socket ├──ROUTER──→│ (ZMQ受信) │
│ (zmq.ROUTER) │ msgpack │ │
│ │ │ │
│ output_socket │←──PULL────┤ (ZMQ送信) │
│ (zmq.PULL) │ msgpack │ │
│ │ │ │
│ outputs_queue │ │ │
│ (asyncio.Queue) │ │ │
└───────────────────┘ └───────────────────┘
主要コンポーネント
| コンポーネント | 用途 | ファイル |
|---|---|---|
EngineCoreClient (ABC) | 抽象インターフェース | target/vllm/vllm/v1/engine/core_client.py:63 |
MPClient | マルチプロセスクライアント基底 | target/vllm/vllm/v1/engine/core_client.py:442 |
AsyncMPClient | 非同期マルチプロセスクライアント(AsyncLLM用) | target/vllm/vllm/v1/engine/core_client.py:822 |
SyncMPClient | 同期マルチプロセスクライアント(LLM用) | target/vllm/vllm/v1/engine/core_client.py |
DPAsyncMPClient | データ並列(外部LB) | target/vllm/vllm/v1/engine/core_client.py |
DPLBAsyncMPClient | データ並列(内部LB) | target/vllm/vllm/v1/engine/core_client.py |
MsgpackEncoder | リクエストのシリアライズ | target/vllm/vllm/v1/serial_utils.py |
MsgpackDecoder | レスポンスのデシリアライズ | target/vllm/vllm/v1/serial_utils.py |
主要メソッド
EngineCoreClient (ABC)
| メソッド | 説明 |
|---|---|
make_client() | ファクトリ。設定に応じた適切なサブクラスを返す |
make_async_mp_client() | AsyncLLM用ファクトリ。DP構成も考慮 |
add_request() | EngineCoreRequestを送信 |
get_output() | EngineCoreOutputsを受信 |
abort_requests() | リクエストキャンセル |
AsyncMPClient
| メソッド | 行 | 説明 |
|---|---|---|
_ensure_output_queue_task() | L856 | ZMQ出力受信タスクを起動 |
get_output_async() | L902 | asyncio.Queueから出力を取得 |
_send_input() | L913 | EngineCoreRequestをZMQで送信 |
_send_input_message() | L925 | ZMQ multipart送信(zero-copy対応) |
ファクトリ選択ロジック
参照: target/vllm/vllm/v1/engine/core_client.py:99 (make_async_mp_client)
make_async_mp_client(vllm_config, executor_class, ...)
├─ data_parallel_size > 1 の場合:
│ ├─ external_lb → DPAsyncMPClient
│ └─ internal_lb → DPLBAsyncMPClient
└─ それ以外 → AsyncMPClient
設定
| パラメータ | デフォルト | 説明 |
|---|---|---|
parallel_config.data_parallel_size | 1 | データ並列数。>1でDP系クライアントを使用 |
parallel_config.data_parallel_external_lb | — | 外部ロードバランサ使用フラグ |
呼び出しフロー
[送信パス]
AsyncLLM.add_request()
→ engine_core.add_request_async(request)
→ AsyncMPClient._send_input(REQUEST, request)
→ MsgpackEncoder.encode(request)
→ input_socket.send_multipart(msg, copy=False)
→ ZMQ ROUTER → バックエンドプロセス
[受信パス]
process_outputs_socket() [バックグラウンドタスク]
→ output_socket.recv_multipart()
→ MsgpackDecoder.decode(frames) → EngineCoreOutputs
→ outputs_queue.put_nowait(outputs)
AsyncLLM._run_output_handler()
→ engine_core.get_output_async()
→ outputs_queue.get() → EngineCoreOutputs
設計上の特徴
- プロセス分離: EngineCoreが別プロセスで動作するため、GILの影響を受けずスケジューリングとGPU実行を並行可能
- msgpackシリアライゼーション:
msgspec.Structのarray_like形式でコンパクトなバイナリ表現 - zero-copy: ZMQ
copy=Falseでメモリコピーを最小化。テンソルバッキングバッファの追跡(add_pending_message) - weakref: 出力タスクがクライアントへの循環参照を持たないよう
weakrefを使用
関連ドキュメント
エントリポイント (AsyncLLM / LLM) サマリー
深度: [SHALLOW] 確信度: [VERIFIED] 最終更新: 2026-02-09
概要
AsyncLLMとLLMはvLLMの2つの主要エントリポイントである。AsyncLLMはAPIサーバー(OpenAI互換API等)が使用する非同期パスで、LLMはオフラインバッチ推論用の同期パスである。どちらもInputProcessorで入力を処理し、EngineCoreClient経由でバックエンド(EngineCore)にリクエストを送信する。
アーキテクチャ
graph LR
subgraph 非同期パス
OpenAI["OpenAI API Server"] --> AsyncLLM
AsyncLLM -->|"process_inputs()"| IP["InputProcessor"]
IP -->|"EngineCoreRequest"| AsyncLLM
AsyncLLM -->|"add_request_async()"| Client["EngineCoreClient"]
Client -->|"ZMQ"| EC["EngineCore"]
end
subgraph 同期パス
User["ユーザーコード"] --> LLM
LLM -->|"process_inputs()"| IP2["InputProcessor"]
IP2 -->|"EngineCoreRequest"| LLM
LLM -->|"add_request()"| Client2["EngineCoreClient"]
end
主要コンポーネント
| コンポーネント | 用途 | ファイル |
|---|---|---|
AsyncLLM | 非同期推論エントリポイント。AsyncGeneratorでストリーミング出力 | target/vllm/vllm/v1/engine/async_llm.py:71 |
LLM | 同期バッチ推論エントリポイント。list[RequestOutput]を返す | target/vllm/vllm/entrypoints/llm.py:101 |
RequestOutputCollector | 非同期パスでの出力キュー管理 | target/vllm/vllm/v1/engine/async_llm.py |
ParentRequest | n>1サンプリング時の親リクエスト管理 | target/vllm/vllm/v1/engine/async_llm.py |
主要メソッド
AsyncLLM
| メソッド | 行 | 説明 |
|---|---|---|
generate() | L537 | メインAPI。AsyncGeneratorでRequestOutputをyield |
add_request() | L286 | リクエスト追加。InputProcessor→OutputProcessor→EngineCore |
_add_request() | L414 | 内部: OutputProcessorとEngineCoreに登録 |
_run_output_handler() | L647 | バックグラウンドタスク起動。EngineCore出力を受信→キュー |
LLM
| メソッド | 行 | 説明 |
|---|---|---|
generate() | L396 | バッチ推論API。list[RequestOutput]を返す |
_add_request() | L1850 | InputProcessor→llm_engine.add_request() |
_run_engine() | L1900 | ポーリングループ。完了までstep()を繰り返す |
設定
| パラメータ | デフォルト | 説明 |
|---|---|---|
log_requests | True | リクエストログ出力 |
log_stats | 引数指定 | 統計ログ出力 |
start_engine_loop | True | エンジンループ自動起動 |
呼び出しフロー
[APIサーバー or ユーザーコード]
→ AsyncLLM.generate() / LLM.generate()
→ InputProcessor.process_inputs()
→ EngineCoreRequest
→ EngineCoreClient.add_request_async()
→ ZMQ → EngineCore(別プロセス)
[バックグラウンド output_handler タスク]
→ EngineCoreClient.get_output_async()
→ EngineCoreOutputs
→ OutputProcessor.process_outputs()
→ RequestOutput → キューにpush
[generate() AsyncGenerator]
→ キューから取り出してyield
関連ドキュメント
Executor
深度: [MEDIUM] 確信度: [VERIFIED] 最終更新: 2026-02-14
概要
Executorは、EngineCoreとWorker(GPUModelRunner)の間に位置する実行委譲レイヤーである。collective_rpc()パターンで全Workerに対して同一メソッドを呼び出し、出力ランクのWorkerの結果を返す。単一プロセス、マルチプロセス、Ray分散の3つの実装を持つ。
クラス階層
Executor (ABC) abstract.py:36
├── UniProcExecutor uniproc_executor.py:26
│ └── ExecutorWithExternalLauncher uniproc_executor.py:140
├── MultiprocExecutor multiproc_executor.py:93
└── RayDistributedExecutor ray_executor.py:62
参照: target/vllm/vllm/v1/executor/abstract.py:36 (Executor)
主要メソッド
collective_rpc()
参照: target/vllm/vllm/v1/executor/abstract.py:180 (collective_rpc)
全Workerに対して同一メソッドを実行するRPCメカニズム。
def collective_rpc(
self,
method: str | Callable[..., _R], # メソッド名または関数
timeout: float | None = None,
args: tuple = (),
kwargs: dict | None = None,
non_block: bool = False, # True: Future返却
) -> list[_R] | Future[list[_R]]
execute_model()
参照: target/vllm/vllm/v1/executor/abstract.py:202 (execute_model)
def execute_model(
self,
scheduler_output: SchedulerOutput,
non_block: bool = False,
) -> ModelRunnerOutput | None | Future[ModelRunnerOutput | None]:
output = self.collective_rpc("execute_model",
args=(scheduler_output,),
non_block=non_block)
return output[0] # 出力ランクWorkerの結果のみ返す
sample_tokens()
参照: target/vllm/vllm/v1/executor/abstract.py:222 (sample_tokens)
def sample_tokens(
self,
grammar_output: GrammarOutput | None,
non_block: bool = False,
) -> ModelRunnerOutput | Future[ModelRunnerOutput]:
output = self.collective_rpc("sample_tokens",
args=(grammar_output,),
non_block=non_block)
return output[0]
実装の使い分け
| 実装 | 用途 | Worker配置 | 特徴 |
|---|---|---|---|
UniProcExecutor | 単一GPU | ドライバプロセス内 | 最小オーバーヘッド。max_concurrent_batches > 1時はThreadPoolExecutor使用 |
MultiprocExecutor | 複数GPU(TP/PP) | 子プロセス | MessageQueue(共有メモリ)ベース。Pipeline Parallelism対応 |
RayDistributedExecutor | 分散クラスタ | Rayアクター | Ray経由のリモートWorker管理 |
MultiprocExecutor のプロセス間通信 [MEDIUM] [VERIFIED]
MultiprocExecutorはSharedMemory MessageQueue(ShmRingBuffer)を使って同一ノード内のWorkerプロセスと通信する。
MessageQueue の仕組み
参照: target/vllm/vllm/distributed/device_communicators/shm_broadcast.py:272 (MessageQueue)
2つのチャネルを併用:
- ShmRingBuffer(共有メモリ): 24MiB以下の通常データ。ロックフリー、~20nsメモリフェンスのみ
- ZMQ PUB/SUB(フォールバック): 24MiBを超えるデータ。ローカルはIPC、リモートはTCP
ShmRingBufferのメモリレイアウト:
┌──────────────────────────────────┬──────────────────────────────────────┐
│ data: chunk0 | chunk1 | ... | N │ metadata: [written|r0|r1|...] × N │
│ max_chunks × 24MiB │ max_chunks × (1 + n_reader) bytes │
└──────────────────────────────────┴──────────────────────────────────────┘
メタデータフラグで書き込み/読み取り状態を管理。全readerが読み取り完了するとチャンクが再利用される。
キュー構成
| キュー | 方向 | 用途 |
|---|---|---|
rpc_broadcast_mq | Executor → 全Worker | RPCコマンドのブロードキャスト |
worker_response_mq × N | 各Worker → Executor | 実行結果の返送 |
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:131-136 (rpc_broadcast_mq生成)
collective_rpc の動作フロー
MultiprocExecutor.collective_rpc("execute_model", args=(...))
│
├─ rpc_broadcast_mq.enqueue((method, args, kwargs, output_rank))
│ → pickle → ShmRingBuffer書き込み → メモリフェンス
│
├─ Worker-0: dequeue() → execute → response_mq.enqueue()
├─ Worker-1: dequeue() → execute → response_mq.enqueue()
│
└─ Executor: response_mqs[output_rank].dequeue() → 結果返却
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:303-375 (collective_rpc)
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:845-871 (worker_busy_loop)
Worker プロセスの起動とビジーループ
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:696 (WorkerProc.worker_main)
WorkerProc.worker_main()
├─ Worker.init_device()
│ └─ torch.distributed.init_process_group(backend="nccl")
├─ Worker.load_model()
├─ READY送信(Pipe経由)
└─ worker_busy_loop():
while True:
method, args, kwargs, output_rank = rpc_broadcast_mq.dequeue()
output = getattr(worker, method)(*args, **kwargs)
worker_response_mq.enqueue((SUCCESS, output))
Worker(委譲先)
参照: target/vllm/vllm/v1/worker/gpu_worker.py:70 (Worker)
Worker(WorkerBase) はGPUModelRunnerのラッパーで、以下の追加処理を行う:
- Pipeline Parallelism: 前段ランクからの
IntermediateTensors受信、後段への送信 - 推論モード管理:
@torch.inference_mode()デコレータ
Worker.execute_model(scheduler_output) # L604
├─ PP: recv_tensor_dict() → IntermediateTensors # L614-641
├─ model_runner.execute_model(scheduler_output, ...) # L652
│ → ModelRunnerOutput | None | IntermediateTensors
├─ PP: send_tensor_dict(intermediate_tensors) # L660-671
└─ return ModelRunnerOutput | None
EngineCore → 出力 の委譲フロー
EngineCore.step()
└─ executor.execute_model(scheduler_output, non_block=True)
└─ collective_rpc("execute_model")
└─ Worker.execute_model()
└─ GPUModelRunner.execute_model()
→ ExecuteModelState 保存、None 返却
EngineCore.step()(続き)
└─ executor.sample_tokens(grammar_output)
└─ collective_rpc("sample_tokens")
└─ Worker.sample_tokens()
└─ GPUModelRunner.sample_tokens()
→ ModelRunnerOutput 返却
上流・下流の関係
- 上流: EngineCore(
step()から呼び出し) - 下流: Worker → GPUModelRunner
Phase 2 深堀り候補
MultiprocExecutorのMessageQueue実装詳細→ 調査済み(本ドキュメント)- Pipeline Parallelism時のバッチスケジューリング
- Ray分散実行のオーバーヘッドと障害回復
- AsyncScheduling時のasync_output_busy_loop動作
主要ファイル
| ファイル | 主要クラス/関数 |
|---|---|
target/vllm/vllm/v1/executor/abstract.py | Executor, collective_rpc() (L180), execute_model() (L202) |
target/vllm/vllm/v1/executor/uniproc_executor.py | UniProcExecutor (L26) |
target/vllm/vllm/v1/executor/multiproc_executor.py | MultiprocExecutor (L93), WorkerProc (L493), worker_busy_loop (L845) |
target/vllm/vllm/v1/executor/ray_executor.py | RayDistributedExecutor (L62) |
target/vllm/vllm/v1/worker/gpu_worker.py | Worker (L70), execute_model() (L604) |
target/vllm/vllm/v1/worker/worker_base.py | WorkerBase (L34), WorkerWrapperBase (L175) |
target/vllm/vllm/distributed/device_communicators/shm_broadcast.py | ShmRingBuffer (L127), MessageQueue (L272) |
GPUModelRunner
深度: [MEDIUM] 確信度: [VERIFIED] 最終更新: 2026-02-15
概要
GPUModelRunnerは、推論パイプラインの実行中核を担う巨大クラス(約6,300行)である。SchedulerOutputを受け取り、入力テンソルの準備、モデルのforward実行、サンプリングを経て、ModelRunnerOutputを返す。2フェーズ実行パターン(execute_model() → sample_tokens())を採用し、モデルフォワードとgrammar bitmask計算の並行実行を可能にしている。
クラス定義
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:329 (GPUModelRunner)
class GPUModelRunner(
LoRAModelRunnerMixin, # LoRAアダプタ管理
KVConnectorModelRunnerMixin, # KV Transfer対応
ECConnectorModelRunnerMixin, # エンコーダキャッシュ対応
):
状態管理
GPUModelRunnerは2つのデータ構造でリクエスト状態を管理する:
self.requests: dict[str, CachedRequestState]— リクエストの論理状態(プリエンプション後も保持)self.input_batch: InputBatch— 永続バッチテンソル群(事前割り当て、差分更新)
永続バッチ最適化により、連続stepで変更があったリクエストのデータのみCPU側で更新し、GPUへはDMA一括転送する。
詳細は InputBatch: 永続バッチと状態管理 を参照。
KVCache-GPU Interface
KVCacheManager(Scheduler側)が割り当てた論理ブロックIDは、4段階の変換を経てAttentionカーネルが消費する形式になる:
_update_states() ← ブロックID取込(3ケース: 新規/追加/プリエンプション復帰)
↓
BlockTable ← CPU側テーブル(CpuGpuBuffer)
↓
_prepare_inputs() ← commit_block_table() DMA + compute_slot_mapping()
↓
_get_slot_mappings() ← by_gid(AttentionMetadata用)/ by_layer(ForwardContext用)
↓
_build_attention_metadata() ← CommonAttentionMetadata → per-layer AttentionMetadata
核心的な変換式: slot = block_number * block_size + (position % block_size)
詳細は KVCache-GPU Interface を参照。
2フェーズ実行パターン
GPUModelRunnerの中核設計。execute_model()でモデルフォワードとlogits計算を行い、結果をExecuteModelStateに保存してNoneを返す。その後sample_tokens()が状態を復元してサンプリングを実行する。
sequenceDiagram
participant EC as EngineCore
participant MR as GPUModelRunner
EC->>MR: execute_model(scheduler_output)
Note over MR: 入力準備 → モデルフォワード<br>→ logits計算 → 状態保存
MR-->>EC: None
Note over EC: grammar bitmask 計算<br>(並行処理)
EC->>MR: sample_tokens(grammar_output)
Note over MR: 状態復元 → grammar適用<br>→ サンプリング → 出力構築
MR-->>EC: ModelRunnerOutput
Phase 1: execute_model()
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:3312 (execute_model)
execute_model(scheduler_output, intermediate_tensors=None)
│
├─ 1. バッチ状態更新 _update_states() # L3376
│ 新規リクエスト登録、ブロックID更新、不要リクエスト除去
│
├─ 2. 入力準備 _prepare_inputs() # L3383
│ block_table DMA → slot_mapping計算 → positions/input_ids GPU転送
│ ※ DMAオーバーラップ最適化(L1472-1474)
│
├─ 3. CUDAGraph判定 # L3398
│ _determine_batch_execution_and_padding()
│ → CUDAGraphMode(FULL/PIECEWISE/NONE) + パディング量決定
│
├─ 4. スロットマッピング2形式出力 # L3468
│ _get_slot_mappings() → by_gid + by_layer
│
├─ 5. Attentionメタデータ構築 # L3479
│ _build_attention_metadata()
│ → CommonAttentionMetadata → per-layer AttentionMetadata
│
├─ 6. モデルフォワード # L3538
│ set_forward_context(slot_mapping=by_layer)
│ → _model_forward() → model.forward() → logits
│
└─ 7. 状態保存 # L3605-3615
ExecuteModelState に保存 → None を返す
戻り値のパターン:
None— 通常ケース(sample_tokens()を後で呼ぶ)ModelRunnerOutput— プーリングモデル等(サンプリング不要)IntermediateTensors— Pipeline Parallelismの中間ランクEMPTY_MODEL_RUNNER_OUTPUT— スケジュールトークン0件
Phase 2: sample_tokens()
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:3621 (sample_tokens)
sample_tokens(grammar_output)
│
├─ 1. 状態復元 — ExecuteModelState から logits 等を復元 # L3643-3657
├─ 2. Grammar制約適用 — bitmask → logits # L3659-3663
├─ 3. サンプリング — _sample() → SamplerOutput # L3665-3666
├─ 4. 後処理 — バッチ状態反映、PP broadcast、ドラフト提案 # L3668-3699
└─ 5. ModelRunnerOutput構築 # L3775-3787
CUDAGraph統合 [VERIFIED]
CUDAGraphMode
3つの実行モードが存在する:
| モード | 説明 | 使用条件 |
|---|---|---|
FULL | forward全体をキャプチャ | Attentionバックエンドが対応、cascade attention無効 |
PIECEWISE | Attention以外をキャプチャ(torch.compile統合) | Attention部分はコンパイル済みコードで実行 |
NONE | Eagerモード | CUDAGraph無効、バッチサイズ超過、calc_kv_scales時 |
CudagraphDispatcher
参照: target/vllm/vllm/v1/cudagraph_dispatcher.py:14
事前キャプチャ済みCUDAGraphの中からランタイムで適切なグラフを選択する:
cudagraph_keys: dict[CUDAGraphMode, set[BatchDescriptor]]にキャプチャ済みのバッチ記述子を保持dispatch()は入力num_tokensを最小のパディングサイズに丸め上げ- FULL → PIECEWISE → NONE の優先順序でキーを探索
バッチパディング: CUDAGraphは固定形状のテンソルを要求するため、実際のトークン数をキャプチャ済みサイズに丸め上げる。未使用スロットはslot_mapping -1(PAD_SLOT_ID)で埋められ、reshape_and_cache() がスキップする。
_determine_batch_execution_and_padding()
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:3076
毎stepの実行モード判定:
_is_uniform_decode()— 全リクエストがデコードフェーズ(query_len=1)か判定cudagraph_dispatcher.dispatch()— モードとパディングサイズを決定- Data Parallel時は
coordinate_batch_across_dp()で全ランク間で合意
ExecuteModelState
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:313 (ExecuteModelState)
2フェーズ間の一時状態を保持するNamedTuple。GPUテンソルを含むため、シリアライズはされない。
| フィールド | 型 | 説明 |
|---|---|---|
scheduler_output | SchedulerOutput | スケジュール結果 |
logits | torch.Tensor | モデル出力logits |
spec_decode_metadata | SpecDecodeMetadata | None | Speculative Decoding情報 |
hidden_states | torch.Tensor | 隠れ状態 |
sample_hidden_states | torch.Tensor | サンプリング用隠れ状態 |
aux_hidden_states | list[torch.Tensor] | None | 補助隠れ状態 |
ec_connector_output | ECConnectorOutput | None | エンコーダ出力 |
cudagraph_stats | CUDAGraphStat | None | CUDAGraph統計 |
slot_mappings | dict | list | None | KVキャッシュスロットマッピング |
6,300行の内訳 [VERIFIED]
| 行範囲 | セクション |
|---|---|
| 1-312 | インポート、型エイリアス、Async出力クラス、ExecuteModelState |
| 329-712 | __init__(設定、バッファ割当、状態初期化) |
| 713-873 | ライフサイクルヘルパー(reset_mm_cache, init_fp8_kv_scales 等) |
| 874-1453 | 状態管理: _update_states(), _update_states_after_model_execute() |
| 1454-1672 | 入力準備: _prepare_inputs(), _prepare_input_ids() |
| 1673-2049 | _build_attention_metadata()(Attentionメタデータ構築) |
| 2050-2557 | 位置計算、MMエンコーダ実行 |
| 2558-3311 | モデルユーティリティ、_get_slot_mappings() |
| 3312-3620 | execute_model()(メインforward) |
| 3621-3934 | sample_tokens()、PP broadcast、ドラフト提案 |
| 3935-4118 | Speculative Decoding提案 |
| 4119-4609 | モデルロード: load_model(), reload_weights() |
| 4610-5108 | ダミー実行、プロファイリング |
| 5109-5332 | CUDAGraphキャプチャ: capture_model(), _capture_cudagraphs() |
| 5333-5596 | Attentionバックエンド初期化、メタデータビルダー |
| 5597-6152 | KVキャッシュ初期化: initialize_kv_cache() |
| 6152-6273 | get_kv_cache_spec(), タイミング統計 |
マルチモーダル処理
マルチモーダル推論時、GPUModelRunnerは execute_model() 内で以下の追加処理を行う:
_execute_mm_encoder()(L2293):model.embed_multimodal()でビジョンエンコーダ実行。結果をencoder_cache[mm_hash]に格納_gather_mm_embeddings()(L2449):encoder_cacheからスケジュール範囲に対応する埋め込みをスライス。チャンクPrefill対応embed_input_ids():masked_scatter_でテキスト + ビジョン埋め込みをマージ →inputs_embedsとして model.forward() に渡す
encoder_cache: dict[str, torch.Tensor] はGPU上のシンプルなdictキャッシュで、Schedulerの free_encoder_mm_hashes 指示で解放される。
詳細は バックエンド MM処理パス を参照。
上流・下流の関係
- 上流: Worker(
execute_model()/sample_tokens()経由で呼び出し) - 下流: モデル層(
model.forward())、Sampler
深堀り候補(今後)
| テーマ | 関連メソッド | ユーザー関心 |
|---|---|---|
| サンプリング実装 | _sample(), Sampler | 低 |
| KV Transfer連携 | KVConnectorModelRunnerMixin | 高(ユーザー関心2位。次セッション候補) |
| Speculative Decoding | propose_draft_token_ids() | 中 |
| async_scheduling | _update_states_after_model_execute() | 中 |
主要ファイル
| ファイル | 主要クラス/関数 |
|---|---|
target/vllm/vllm/v1/worker/gpu_model_runner.py | GPUModelRunner (L329), execute_model() (L3312), sample_tokens() (L3621), ExecuteModelState (L313) |
target/vllm/vllm/v1/worker/gpu_input_batch.py | CachedRequestState (L30), InputBatch (L81) |
target/vllm/vllm/v1/worker/block_table.py | BlockTable (L16), MultiGroupBlockTable (L253) |
target/vllm/vllm/v1/cudagraph_dispatcher.py | CudagraphDispatcher (L14) |
target/vllm/vllm/v1/utils.py | CpuGpuBuffer (L105) |
target/vllm/vllm/v1/outputs.py | ModelRunnerOutput (L160), AsyncModelRunnerOutput (L200) |
InputBatch: 永続バッチと状態管理
深度: [MEDIUM] 確信度: [VERIFIED] 最終更新: 2026-02-14
概要
GPUModelRunnerは2つのデータ構造でリクエスト状態を管理する:
CachedRequestState— リクエストごとの論理状態(step間で永続、プリエンプション後も保持)InputBatch— 全リクエストの物理バッチテンソル群(事前割り当て、step間でCPU側を差分更新)
永続バッチ最適化により、連続するstep間でリクエストの大部分が同じであることを前提に、差分のみを更新する。
CachedRequestState [VERIFIED]
参照: target/vllm/vllm/v1/worker/gpu_input_batch.py:30
@dataclass で定義されるリクエスト単位の状態。GPUModelRunner.requests: dict[str, CachedRequestState] に格納される。
| フィールド | 型 | 説明 |
|---|---|---|
req_id | str | リクエストID |
prompt_token_ids | list[int] | None | プロンプトトークンID列 |
prompt_embeds | Tensor | None | プロンプト埋め込み(embedsモード時) |
mm_features | list[MultiModalFeatureSpec] | マルチモーダル特徴量 |
sampling_params | SamplingParams | None | サンプリングパラメータ |
generator | torch.Generator | None | シード付き乱数生成器 |
block_ids | tuple[list[int], ...] | KVキャッシュグループごとのブロックID列 |
num_computed_tokens | int | 計算済みトークン数(プレフィックスキャッシュ含む) |
output_token_ids | list[int] | 生成済みトークンID列 |
lora_request | LoRARequest | None | LoRAアダプタ |
ライフサイクル: _update_states() で作成され、finished_req_ids で削除される。プリエンプション時もInputBatchからは除去されるが self.requests には保持され、復帰時に再利用される。
num_tokens プロパティ: num_prompt_tokens + len(output_token_ids) — 現在のリクエスト全体のトークン数。
InputBatch [VERIFIED]
参照: target/vllm/vllm/v1/worker/gpu_input_batch.py:81
全リクエストの状態を事前割り当てテンソル群で管理するバッチマネージャー。
主要テンソル群
| テンソル | shape | 種別 | 説明 |
|---|---|---|---|
token_ids_cpu_tensor | (max_num_reqs, max_model_len) | CPU (非pin) | 全リクエストのトークンID。大きくなりうる(TODOコメントあり) |
num_computed_tokens_cpu_tensor | (max_num_reqs,) | CPU (pin) | 計算済みトークン数 |
num_tokens_no_spec | (max_num_reqs,) | numpy | Spec Decode以外のトークン数 |
num_prompt_tokens | (max_num_reqs,) | numpy | プロンプトトークン数 |
temperature_cpu / top_p_cpu / top_k_cpu | (max_num_reqs,) | CPU (pin) | サンプリングパラメータ |
block_table | MultiGroupBlockTable | CpuGpuBuffer | KVキャッシュブロックテーブル |
リクエスト管理
| データ構造 | 説明 |
|---|---|
_req_ids: list[str | None] | インデックス→リクエストID。None は空スロット |
req_id_to_index: dict[str, int] | リクエストID→インデックス。逆引き |
num_reqs プロパティ | len(req_id_to_index) — 現在のリクエスト数 |
add_request() / remove_request() [VERIFIED]
参照: target/vllm/vllm/v1/worker/gpu_input_batch.py:304, 469
add_request():
- 空きインデックスを探す(末尾追加 or 空スロット再利用)
token_ids_cpuにプロンプト+出力トークンIDをコピーblock_table.add_row()でブロックIDを設定- サンプリングパラメータを各テンソルにコピー
remove_request():
req_id_to_indexから削除、_req_ids[index] = Noneで空スロット化batch_update_builder.removed_append()で空きインデックスを記録- サンプリング関連のset/dictからも除去
- テンソル自体はクリアしない(次のadd_requestで上書きされる)
condense() — バッチ圧縮 [VERIFIED]
参照: target/vllm/vllm/v1/worker/gpu_input_batch.py:626
remove_request() で生じた空スロットを埋める処理。末尾のリクエストを空スロットに移動し、連続した配列を保つ:
[A, _, B, _, C] → [A, C, B] (Cを空スロット1に移動)
移動対象: token_ids_cpu, num_tokens_no_spec, num_prompt_tokens, num_computed_tokens_cpu, block_table.move_row(), サンプリングパラメータ等。
MultiGroupBlockTable [VERIFIED]
参照: target/vllm/vllm/v1/worker/block_table.py:253
KVキャッシュグループごとに BlockTable を保持するラッパー。Hybridモデル(異なるアテンションタイプが混在)では複数のBlockTableが存在する。
# アクセスパターン
input_batch.block_table[kv_cache_gid] # → BlockTable
input_batch.block_table.append_row(block_ids, req_index) # 全グループに委譲
input_batch.block_table.commit_block_table(num_reqs) # 全グループDMA
block_ids は tuple[list[int], ...] 型で、外側タプルのインデックスがKVキャッシュグループIDに対応。
永続バッチ最適化の全体像 [VERIFIED]
sequenceDiagram
participant S as Scheduler
participant US as _update_states()
participant IB as InputBatch
participant PI as _prepare_inputs()
participant GPU as GPU Tensors
S->>US: SchedulerOutput
Note over US: finished_req_ids → remove_request()
Note over US: unscheduled → remove_request()
Note over US: new_reqs → CachedRequestState作成
Note over US: cached_reqs → block_ids更新
US->>IB: add_request() / append_row() / condense()
Note over IB: CPU側テンソルを差分更新
IB->>PI: commit_block_table() (DMA)
PI->>GPU: slot_mapping, positions, input_ids etc.
最適化の本質: 前stepと大部分が同じリクエスト群に対して、変更があったフィールド(新ブロックID、num_computed_tokens等)のみをCPU側で更新し、GPUへはDMA一括転送する。毎step全データを再構築する必要がない。
主要ファイル
| ファイル | 主要クラス/関数 |
|---|---|
target/vllm/vllm/v1/worker/gpu_input_batch.py | CachedRequestState (L30), InputBatch (L81), add_request() (L304), remove_request() (L469), condense() (L626) |
target/vllm/vllm/v1/worker/block_table.py | MultiGroupBlockTable (L253) |
KVCache-GPU Interface: ブロックテーブルとスロットマッピング
深度: [MEDIUM] 確信度: [VERIFIED] 最終更新: 2026-02-14
概要
KVCacheManager(Scheduler側)が割り当てた論理ブロックIDは、GPUModelRunner内で4段階の変換を経てAttentionカーネルが消費できる形式になる。この文書では、ブロックIDの取り込みからAttentionMetadata構築までの完全なデータパスを追跡する。
graph LR
A["KVCacheManager<br>(論理ブロック割当)"] -->|SchedulerOutput<br>new_block_ids| B["_update_states()<br>(ブロックID取込)"]
B -->|CachedRequestState<br>block_ids| C["BlockTable<br>(CPU側テーブル)"]
C -->|"commit_block_table()<br>DMA (non-blocking)"| D["GPU block_table"]
C -->|"compute_slot_mapping()"| E["CPU slot_mapping"]
E -->|"commit_slot_mapping()<br>DMA"| F["GPU slot_mapping"]
D --> G["_build_attention_metadata()"]
F --> G
G -->|"per-layer<br>AttentionMetadata"| H["Attention Kernel"]
F -->|"set_forward_context()<br>slot_mappings_by_layer"| I["reshape_and_cache()"]
Stage A: ブロックID取込 — _update_states() [VERIFIED]
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:874
SchedulerOutputには3種類のリクエストデータが含まれ、それぞれブロックIDの扱いが異なる:
| ケース | block_ids の処理 | InputBatch操作 |
|---|---|---|
| 新規リクエスト | new_req_data.block_ids → CachedRequestState 作成 | add_request() で追加 |
| 継続(非プリエンプション) | new_block_ids を既存 block_ids に extend() | block_table.append_row() で差分追加 |
| プリエンプション復帰 | block_ids を丸ごと new_block_ids で置換 | add_request() で再追加(前stepで remove_request() 済み) |
block_ids の型は list[list[int]] — 外側のリストはKVキャッシュグループ(Hybridモデルで複数)、内側はそのグループのブロックID列。
重要な最適化 — 永続バッチ: 前stepで存在し今回もスケジュールされたリクエストは、InputBatchに残り続ける。スケジュールされなかったリクエストだけが remove_request() で除去されるが、CachedRequestState は self.requests に保持される(L901-921)。
Stage B: スロットマッピング計算 — BlockTable.compute_slot_mapping() [VERIFIED]
参照: target/vllm/vllm/v1/worker/block_table.py:133
トークンの論理位置を、KVキャッシュの物理GPUメモリスロットに変換する式:
block_table_index = req_index * max_num_blocks_per_req + position // block_size
block_number = block_table[block_table_index] # ravel()で1D化してルックアップ
block_offset = position % block_size
slot = block_number * block_size + block_offset
Hybrid Block対応
KVCacheManagerの割当ブロックサイズとAttentionカーネルのブロックサイズが異なる場合(例: 割当=32トークン、カーネル=16トークン):
参照: target/vllm/vllm/v1/worker/block_table.py:203 (map_to_kernel_blocks)
# 割当ブロックID [0, 1, 2] → カーネルブロックID [0, 1, 2, 3, 4, 5]
# ブロック0 → [0, 1], ブロック1 → [2, 3], ブロック2 → [4, 5]
kernel_ids = block_id * blocks_per_kv_block + arange(blocks_per_kv_block)
append_row() と add_row() は、ブロックIDを追加する前にこの変換を適用する。compute_slot_mapping() では変換後の kernel_block_size が block_size として使用される。
Context Parallel対応
CP(DCP/PCP)有効時は「仮想ブロック」サイズ(= block_size * cp_world_size)を使ってブロックテーブルインデックスを算出し、ローカルランクに属さないトークンのスロットを -1 にマスクする(L142-179)。
Stage C: GPU転送 — _prepare_inputs() [VERIFIED]
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:1454
_prepare_inputs() は以下の順序でCPU→GPU転送を実行する:
1. commit_block_table(num_reqs) ← 最初に発行(DMAオーバーラップ最適化)
2. positions計算 (CPU) ← DMA中にCPU演算
3. token_indices計算 (CPU) ← DMA中にCPU演算
4. compute_slot_mapping() ← CPU演算
5. commit_slot_mapping() ← DMA発行
6. query_start_loc → GPU
7. seq_lens → GPU
8. input_ids → GPU
9. positions → GPU
最適化ポイント: commit_block_table() をCPU演算の前に呼ぶことで、ブロックテーブルのDMA転送とCPU上のslot_mapping/positions計算を並行実行する(L1472-1474のコメント)。
CpuGpuBuffer
参照: target/vllm/vllm/v1/utils.py:105
ピン留めメモリCPUテンソル + GPUテンソルのペア。np 属性でnumpyビューを公開し、CPU側の演算をnumpy高速演算で行える。copy_to_gpu(n) は non_blocking=True でDMA転送を発行。
| 属性 | 型 | 説明 |
|---|---|---|
cpu | torch.Tensor (pinned) | ピン留めCPUメモリ |
gpu | torch.Tensor | GPU VRAM |
np | np.ndarray | cpu のnumpyビュー |
Stage D: 2形式のスロットマッピング出力 — _get_slot_mappings() [VERIFIED]
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:3237
_get_slot_mappings() はスロットマッピングを2つの形式で返す:
| 形式 | キー | 消費者 |
|---|---|---|
slot_mappings_by_gid | dict[int, Tensor] — KVキャッシュグループID | _build_attention_metadata() → CommonAttentionMetadata.slot_mapping |
slot_mappings_by_layer | dict[str, Tensor] — レイヤー名 | set_forward_context() → reshape_and_cache() |
同一KVキャッシュグループ内のレイヤーは同じスロットマッピングテンソルを共有する。
CUDAGraph FULLモード時のパディング: 未使用トークンスロットは -1 で埋められる(L3283-3285)。これはMambaの PAD_SLOT_ID と一致し、reshape_and_cache() が -1 スロットをスキップする。
Stage E: Attentionメタデータ構築 — _build_attention_metadata() [VERIFIED]
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:1673
CommonAttentionMetadata
全KVキャッシュグループの共通基盤。グループ0のblock_table/slot_mappingで初期化し、他グループでは copy() (shallow copy) してblock_tableとslot_mappingのみ差し替える。
cm_base = CommonAttentionMetadata(
query_start_loc=..., # GPU tensor [num_reqs+1]
seq_lens=..., # GPU tensor [num_reqs]
block_table_tensor=..., # GPU tensor [num_reqs, max_blocks] ← グループ0
slot_mapping=..., # GPU tensor [num_tokens] ← グループ0
max_query_len=...,
max_seq_len=...,
...
)
Per-layer AttentionMetadata構築
graph TD
A[CommonAttentionMetadata] -->|"copy() + グループ別<br>block_table/slot_mapping"| B["cm (per kv_cache_group)"]
B --> C["AttentionMetadataBuilder.build()"]
C --> D["AttentionMetadata"]
D -->|"同一attn_group内の<br>全レイヤーが共有"| E["attn_metadata[layer_name]"]
_build_attn_group_metadata() 内部関数が各(kv_cache_gid, attn_gid)ペアに対して:
attn_group.get_metadata_builder()でビルダーを取得builder.build(common_attn_metadata=cm)でAttentionMetadataを生成attn_group.layer_namesの全レイヤーに同じメタデータを割り当て
キャッシュ最適化
Hybridモデルで同じ KVCacheSpec + 同じビルダー型の組み合わせが複数グループに現れる場合、2番目以降は builder.update_block_table() で block_table のみ差し替え、ビルド全体をスキップする(L1824-1832)。
戻り値の型
PerLayerAttnMetadata = dict[str, AttentionMetadata] # 通常
| list[dict[str, AttentionMetadata]] # UBatching(DBO)時
execute_model() 内の呼び出し順序 [VERIFIED]
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:3312
execute_model(scheduler_output)
│
├─ 1. _prepare_inputs() # L3383
│ block_table DMA + slot_mapping計算 + GPU転送
│
├─ 2. _determine_batch_execution_and_padding() # L3398
│ CUDAGraphモード判定 → num_tokens_padded, num_reqs_padded
│
├─ 3. _get_slot_mappings() # L3468
│ 2形式出力: by_gid (→ attn_metadata), by_layer (→ ForwardContext)
│
├─ 4. _build_attention_metadata() # L3479
│ CommonAttentionMetadata → per-layer AttentionMetadata
│
├─ 5. set_forward_context(slot_mapping=slot_mappings_by_layer) # L3524
│ ForwardContext にスロットマッピングを設定
│
└─ 6. _model_forward() # L3538
model.forward() → Attention layer が ForwardContext から
slot_mapping を取得し reshape_and_cache() を実行
主要ファイル
| ファイル | 主要クラス/関数 |
|---|---|
target/vllm/vllm/v1/worker/gpu_model_runner.py | _update_states() (L874), _prepare_inputs() (L1454), _build_attention_metadata() (L1673), _get_slot_mappings() (L3237), execute_model() (L3312) |
target/vllm/vllm/v1/worker/block_table.py | BlockTable (L16), compute_slot_mapping() (L133), map_to_kernel_blocks() (L203), MultiGroupBlockTable (L253) |
target/vllm/vllm/v1/utils.py | CpuGpuBuffer (L105) |
InputProcessor サマリー
深度: [SHALLOW] 確信度: [VERIFIED] 最終更新: 2026-02-09
概要
InputProcessorはユーザー入力(テキストプロンプト、SamplingParams等)を内部表現EngineCoreRequestに変換するコンポーネントである。トークナイズ、パラメータのバリデーションと正規化、マルチモーダル入力の前処理を担当する。AsyncLLMの初期化時に生成され、フロントエンドプロセスで動作する。
アーキテクチャ
graph LR
Prompt["PromptType<br>(str / list[int] / dict)"] --> IP["InputProcessor"]
Params["SamplingParams"] --> IP
IP --> IPP["InputPreprocessor<br>tokenizer.encode()"]
IPP --> PI["ProcessorInputs"]
PI --> IP
IP --> ECR["EngineCoreRequest"]
主要コンポーネント
| コンポーネント | 用途 | ファイル |
|---|---|---|
InputProcessor | 入力処理のメインクラス | target/vllm/vllm/v1/engine/input_processor.py:56 |
InputPreprocessor | トークナイズとマルチモーダル前処理 | target/vllm/vllm/v1/engine/input_processor.py (内部利用) |
ProcessorInputs | 前処理結果の中間データ構造 | target/vllm/vllm/v1/engine/input_processor.py |
主要メソッド
| メソッド | 行 | 入力 | 出力 |
|---|---|---|---|
process_inputs() | L521 | request_id, prompt, params | EngineCoreRequest |
assign_request_id() | (別メソッド) | EngineCoreRequest | None (内部IDを付与) |
_validate_lora() | L535付近 | LoRARequest | バリデーション |
_validate_params() | L536付近 | SamplingParams | バリデーション |
process_inputs() の処理フロー
process_inputs(request_id, prompt, params)
1. バリデーション
├─ LoRAリクエスト検証
├─ パラメータ検証
└─ data_parallel_rank 範囲チェック
2. arrival_time 設定
3. input_preprocessor.preprocess(prompt)
→ テキストをトークナイズ → ProcessorInputs
4. split_enc_dec_inputs()
→ エンコーダ/デコーダ入力を分離
5. SamplingParams 正規化
├─ clone() で複製
├─ max_tokens 未設定時: max_model_len - seq_len
├─ update_from_generation_config()
└─ update_from_tokenizer()
6. EngineCoreRequest を構築して返す
設定
| パラメータ | デフォルト | 説明 |
|---|---|---|
model_config.max_model_len | モデル依存 | max_tokens未指定時の上限計算に使用 |
cache_config.enable_prefix_caching | — | マルチモーダルUUID生成方式に影響 |
呼び出しフロー
AsyncLLM.add_request() / LLM._add_request()
→ InputProcessor.process_inputs()
→ InputPreprocessor.preprocess()
→ tokenizer.encode()
→ EngineCoreRequest を返す
→ InputProcessor.assign_request_id()
→ 外部IDを external_req_id に退避
→ 内部ID(外部ID + 8文字ランダム)を request_id に設定
マルチモーダル処理
テキスト入力に加えて画像等のマルチモーダルデータがある場合、InputProcessorは以下の追加処理を行う:
- MM Registry から
BaseMultiModalProcessorを取得(_get_mm_processor()) - マルチモーダルデータのパース:
mm_processor.info.parse_mm_data()→MultiModalDataItems - HF Processor 実行:
mm_processor.apply()→MultiModalInputs(トークン列 + テンソル + ハッシュ + PlaceholderRange) - ProcessorCache:
mm_processor_cacheによるHF処理結果のキャッシュ(4種類: processor_only/lru/shm/none) - MultiModalFeatureSpec 構築: データ + 位置情報 + ハッシュを
EngineCoreRequest.mm_featuresにセット
詳細は マルチモーダル フロントエンド処理 を参照。
関連ドキュメント
KVCacheManager サマリー
深度: [DEEP] 確信度: [VERIFIED] 最終更新: 2026-02-11
概要
KVCacheManager は PagedAttention に基づく KV キャッシュブロックの割り当て・解放・プレフィックスキャッシュ検索を管理するクラスである。4層の階層設計(KVCacheManager → KVCacheCoordinator → SingleTypeKVCacheManager → BlockPool)でマルチグループ KV キャッシュを統括する。Scheduler から呼び出され、各リクエストに必要な GPU メモリブロックを確保する。
アーキテクチャ
クラス階層
graph TD
KVM["KVCacheManager<br>公開 API"]
Factory["get_kv_cache_coordinator()<br>ファクトリ関数"]
NPC["NoPrefixCache<br>キャッシュ無効時"]
UC["UnitaryCoordinator<br>単一グループ"]
HC["HybridCoordinator<br>複数グループ"]
FAM["FullAttentionManager"]
SWM["SlidingWindowManager"]
CLM["ChunkedLocalManager"]
MM["MambaManager"]
CAM["CrossAttentionManager"]
SFM["SinkFullAttentionManager"]
BP["BlockPool<br>物理ブロック管理"]
BH["BlockHashToBlockMap"]
FQ["FreeKVCacheBlockQueue"]
KB["KVCacheBlock"]
KVM --> Factory
Factory --> NPC
Factory --> UC
Factory --> HC
UC --> FAM
UC --> SWM
HC --> FAM
HC --> SWM
HC --> CLM
HC --> MM
HC --> CAM
HC --> SFM
FAM --> BP
SWM --> BP
CLM --> BP
MM --> BP
CAM --> BP
SFM --> BP
BP --> BH
BP --> FQ
FQ --> KB
Coordinator 選択ロジック
参照: target/vllm/vllm/v1/core/kv_cache_coordinator.py:542
get_kv_cache_coordinator()
├─ enable_caching == False → KVCacheCoordinatorNoPrefixCache
├─ kv_cache_groups == 1 → UnitaryKVCacheCoordinator
└─ kv_cache_groups > 1 → HybridKVCacheCoordinator
| Coordinator | 用途 | find_longest_cache_hit |
|---|---|---|
NoPrefixCache | キャッシュ無効 | 空リスト、0 トークン |
Unitary | 単一アテンションタイプ | 単一 Manager に委譲 |
Hybrid | 複数アテンションタイプ | 反復固定点アルゴリズム |
KV キャッシュグループ
KV キャッシュグループとは、同一の KVCacheSpec(アテンションタイプ・ブロックサイズ)を共有するモデルレイヤーの集合である。
| モデル例 | グループ構成 |
|---|---|
| 全層 Full Attention | 1 グループ |
| 12 層 Full + 12 層 Sliding Window | 2 グループ |
| デコーダ + クロスアテンション | 2-3 グループ |
各グループは独立した SingleTypeKVCacheManager を持ち、異なるキャッシュ検索アルゴリズム・スキップポリシーを適用する。
KVCacheBlocks [DEEP] [VERIFIED]
参照: target/vllm/vllm/v1/core/kv_cache_manager.py:21
Scheduler と KVCacheManager のインターフェース。内部データ構造を隠蔽する。
@dataclass
class KVCacheBlocks:
blocks: tuple[Sequence[KVCacheBlock], ...]
# blocks[i][j] = i 番目の kv_cache_group、j 番目のブロック
| メソッド | 説明 |
|---|---|
__add__ | 2 つの KVCacheBlocks を結合 |
get_block_ids() | tuple[list[int], ...] に変換(GPU カーネル用) |
get_unhashed_block_ids() | 未ハッシュブロックの ID リスト(ドラフトトークン用) |
new_empty() | 空の KVCacheBlocks を生成 |
GC 最適化: KVCacheManager は empty_kv_cache_blocks を事前生成し、空の結果を返す際に再利用する。
ブロック配置図(allocate_slots)
allocate_slots() がリクエストに割り当てるブロックの論理構造:
| comp | new_comp | ext_comp | new | lookahead |
|<------ 既計算トークン ------>|<-- 新規計算対象 -->|
|<- 割り当て対象 ->|
comp:request.num_computed_tokens— 前ステップまでに計算済みnew_comp:num_new_computed_tokens— プレフィックスキャッシュから新規にヒットしたトークンext_comp:num_external_computed_tokens— KV コネクタ(LMCache 等)から取得したトークンnew:num_new_tokens— 今回計算するトークンlookahead:num_lookahead_tokens— Speculative Decoding 用の先読みトークン
主要コンポーネント
| コンポーネント | 用途 | ファイル |
|---|---|---|
KVCacheManager | Scheduler 向け公開 API | target/vllm/vllm/v1/core/kv_cache_manager.py:94 |
KVCacheCoordinator | マルチグループ統括(3 実装) | target/vllm/vllm/v1/core/kv_cache_coordinator.py:28 |
SingleTypeKVCacheManager | アテンションタイプ別管理(7 実装) | target/vllm/vllm/v1/core/single_type_kv_cache_manager.py:24 |
BlockPool | 物理ブロック割り当て・解放・キャッシュ管理 | target/vllm/vllm/v1/core/block_pool.py:128 |
KVCacheBlock | ブロックメタデータ(block_id, ref_cnt, block_hash) | target/vllm/vllm/v1/core/kv_cache_utils.py:107 |
BlockHashToBlockMap | プレフィックスキャッシュ用ハッシュ→ブロック対応表 | target/vllm/vllm/v1/core/block_pool.py:32 |
FreeKVCacheBlockQueue | LRU 順序の空きブロック管理(双方向リンクリスト) | target/vllm/vllm/v1/core/kv_cache_utils.py:156 |
主要メソッド
| メソッド | 行 | 説明 |
|---|---|---|
allocate_slots() | L206 | リクエストに KV キャッシュブロックを割り当て。成功時 KVCacheBlocks、失敗時 None |
get_computed_blocks() | L164 | プレフィックスキャッシュから最長ヒットを検索。(KVCacheBlocks, int) |
free() | L378 | リクエストのブロックをプールに返却 |
usage (property) | L143 | KV キャッシュ使用率 (0.0-1.0) |
reset_prefix_cache() | L409 | プレフィックスキャッシュ全体をリセット |
get_num_common_prefix_blocks() | L425 | 全リクエスト共通の先頭ブロック数(Cascade Attention 用) |
cache_blocks() | L475 | ブロックをプレフィックスキャッシュに登録 |
allocate_slots() の 5 段階フロー [DEEP] [VERIFIED]
参照: target/vllm/vllm/v1/core/kv_cache_manager.py:206
allocate_slots(request, num_new_tokens, ...)
│
├─ Stage 1: スキップブロック解放(Sliding Window 用)
│ └─ coordinator.remove_skipped_blocks(request_id, total_computed_tokens)
│
├─ Stage 2: 容量チェック
│ ├─ coordinator.get_num_blocks_to_allocate(...)
│ └─ 空きブロック不足 → return None(プリエンプション誘発)
│
├─ Stage 3: キャッシュヒットブロック割り当て
│ └─ new_computed_blocks 非空 or external_computed > 0:
│ └─ coordinator.allocate_new_computed_blocks(...)
│
├─ Stage 4: 新規ブロック割り当て
│ └─ coordinator.allocate_new_blocks(request_id, num_tokens_need_slot, ...)
│
└─ Stage 5: キャッシュ登録判定
├─ NOT enable_caching or delay_cache_blocks → スキップ
└─ coordinator.cache_blocks(request, num_tokens_to_cache)
※ num_tokens_to_cache はドラフトトークンを除外
delay_cache_blocks: P/D(Prefill/Decode 分離)構成で KV Transfer 完了前にキャッシュ登録を遅延する。
プレフィックスキャッシュ
参照: target/vllm/vllm/v1/core/kv_cache_manager.py:164 (get_computed_blocks)
プロンプトトークン列をブロックサイズ単位でハッシュ化し、BlockHashToBlockMap で過去に計算済みのブロックを検索する。ハッシュチェーン(各ブロックのハッシュが前ブロックのハッシュに依存)により、プレフィックスの最長一致を効率的に検索する。
get_computed_blocks(request)
→ coordinator.find_longest_cache_hit(request.block_hashes, max_length)
→ アテンションタイプ別の検索アルゴリズム
→ (キャッシュ済みブロック, ヒットトークン数) を返却
制約: 全トークンがキャッシュヒットしても、logits 取得のため最後の 1 トークンは再計算が必要(max_cache_hit_length = request.num_tokens - 1)。
→ 詳細は プレフィックスキャッシュ詳細 を参照
参照カウントと Eviction
KVCacheBlock (L107) の ref_cnt フィールドでブロックの使用状況を管理:
| ref_cnt | 状態 |
|---|---|
| 0 | 空きブロックキュー(FreeKVCacheBlockQueue)内。Eviction 候補 |
| ≥ 1 | リクエストに使用中。Eviction 対象外 |
- touch(): キャッシュヒット時に
ref_cntを増加し、空きキューから除外 - free_blocks():
ref_cntを減少。0 になったら空きキューに戻す(逆順追加で LRU 効率化) - _maybe_evict_cached_block(): 新規ブロック要求時に空きキューの先頭(最古)から Evict。ハッシュメタデータをリセット
→ 詳細は BlockPool 詳細 を参照
アテンションタイプ対応
7 種の SingleTypeKVCacheManager がアテンションタイプごとのブロック管理を担当:
| Manager | KVCacheSpec | スキップ計算 | キャッシュ検索 |
|---|---|---|---|
FullAttentionManager | FullAttention / MLA | 0 | 左→右 |
SlidingWindowManager | SlidingWindow | max(0, n-w+1) | 右→左(連続) |
ChunkedLocalAttentionManager | ChunkedLocal | (n//c)*c | null_pad + 左→右 |
MambaManager | Mamba | n - 1 | 右→左(単一) |
CrossAttentionManager | CrossAttention | N/A | 非対応 |
SinkFullAttentionManager | SinkFullAttention | 0 | 左→右 |
→ 詳細は アテンションタイプ別 Manager を参照
設定
| パラメータ | デフォルト | 説明 |
|---|---|---|
block_size | モデル依存 | 1 ブロックあたりのトークン数 |
enable_caching | 設定依存 | プレフィックスキャッシュの有効化 |
num_gpu_blocks | プロファイリングで決定 | GPU メモリから算出される総ブロック数 |
hash_block_size | block_size と同値 | ハッシュ計算に使用するブロックサイズ |
prefix_caching_hash_algo | sha256_cbor | ハッシュ関数(sha256/sha256_cbor/xxhash/xxhash_cbor) |
enable_kv_cache_events | False | KV Transfer 用イベント発行 |
呼び出しフロー
Scheduler.schedule()
├─ kv_cache_manager.get_computed_blocks(request) # プレフィックスキャッシュ検索
├─ kv_cache_manager.allocate_slots(request, ...) # ブロック割り当て
│ └─ None の場合 → プリエンプション実行
└─ (完了時)kv_cache_manager.free(request) # ブロック解放
ソースファイル一覧
| ファイル | 行数 | 内容 |
|---|---|---|
kv_cache_manager.py | 490 | KVCacheManager、KVCacheBlocks |
kv_cache_coordinator.py | 586 | Coordinator 3 実装 |
single_type_kv_cache_manager.py | 1065 | Manager 7 種 |
block_pool.py | 490 | BlockPool、BlockHashToBlockMap |
kv_cache_utils.py | 1644 | KVCacheBlock、Queue、ハッシュ計算 |
kv_cache_metrics.py | 96 | メトリクス収集 |
詳細ドキュメント
- BlockPool 詳細 — FreeKVCacheBlockQueue、BlockHashToBlockMap、KVCacheBlock ライフサイクル、Eviction、KV Cache Events
- プレフィックスキャッシュ詳細 — ハッシュチェーン計算、Extra Keys、Lookup アルゴリズム、Hybrid fixed-point
- アテンションタイプ別 Manager — 7 種 Manager の詳細、スキップ計算、キャッシュ検索アルゴリズム
関連ドキュメント
アテンションタイプ別 Manager 詳細
深度: [DEEP] 確信度: [VERIFIED] 最終更新: 2026-02-11
概要
SingleTypeKVCacheManager は1種類のアテンションタイプの KV キャッシュ管理ロジックを担当する抽象基底クラスである。アテンションタイプごとにサブクラスが存在し、ブロックの割り当て・解放・プレフィックスキャッシュ検索をそれぞれのセマンティクスに合わせて実装する。
参照: target/vllm/vllm/v1/core/single_type_kv_cache_manager.py
spec_manager_map [DEEP] [VERIFIED]
参照: target/vllm/vllm/v1/core/single_type_kv_cache_manager.py:1049
spec_manager_map = {
FullAttentionSpec: FullAttentionManager,
MLAAttentionSpec: FullAttentionManager, # MLA も Full 扱い
SlidingWindowSpec: SlidingWindowManager,
ChunkedLocalAttentionSpec: ChunkedLocalAttentionManager,
MambaSpec: MambaManager,
CrossAttentionSpec: CrossAttentionManager,
SinkFullAttentionSpec: SinkFullAttentionManager,
}
get_manager_for_kv_cache_spec(spec, **kwargs) ファクトリ関数がこのマップから Manager クラスをディスパッチする。
基底クラス: SingleTypeKVCacheManager [DEEP] [VERIFIED]
参照: target/vllm/vllm/v1/core/single_type_kv_cache_manager.py:24
状態管理
| フィールド | 型 | 説明 |
|---|---|---|
req_to_blocks | defaultdict[str, list[KVCacheBlock]] | リクエスト ID → 割り当て済みブロックリスト |
num_cached_block | dict[str, int] | リクエスト ID → キャッシュ登録済みブロック数。RUNNING リクエストのみ追跡 |
block_size | int | 1 ブロックあたりのトークン数。DCP/PCP > 1 の場合は乗算される |
コンストラクタ
def __init__(self, kv_cache_spec, block_pool, enable_caching,
kv_cache_group_id, dcp_world_size=1, pcp_world_size=1):
self.block_size = kv_cache_spec.block_size
if dcp_world_size * pcp_world_size > 1:
self.block_size *= dcp_world_size * pcp_world_size
DCP(Decode Context Parallelism)/ PCP(Prefill Context Parallelism)ではブロックサイズが並列度倍に拡大される。
get_num_blocks_to_allocate() [DEEP] [VERIFIED]
参照: L73
リクエストに必要な新規ブロック数を算出する。2 つのパスが存在:
get_num_blocks_to_allocate(request_id, num_tokens, new_computed_blocks, ...)
│
├─ Fast-path: request_id in num_cached_block(RUNNING リクエスト)
│ └─ max(num_required_blocks - num_req_blocks, 0)
│ ※ Speculative Decoding のリジェクトで num_req_blocks > num_required_blocks もあり得る
│
└─ Slow-path: 新規リクエスト(プレフィックスキャッシュヒットあり)
├─ num_skipped_tokens = get_num_skipped_tokens(total_computed_tokens)
├─ num_skipped_blocks = num_skipped_tokens // block_size
├─ num_new_blocks = max(required - max(skipped, local_computed), 0)
├─ num_evictable_blocks = Σ(ref_cnt==0 かつ非null)
│ ← touch() 時にキューから除去されるブロック分を加算
└─ return num_new_blocks + num_evictable_blocks
Evictable blocks の加算理由: new_computed_blocks 内のブロックが空きキュー内(ref_cnt == 0)にある場合、touch() で空きキューから除去されるため、実質的に空きブロック数が減る。この分を事前に計上する。
allocate_new_computed_blocks() [DEEP] [VERIFIED]
参照: L137
プレフィックスキャッシュヒットしたブロックをリクエストに追加する。
allocate_new_computed_blocks(request_id, new_computed_blocks, ...)
│
├─ RUNNING → assert len(new_computed_blocks) == 0 → return
│
└─ 新規リクエスト:
├─ num_skipped_blocks 計算
├─ スキップ分を new_computed_blocks から除去
├─ enable_caching → block_pool.touch(new_computed_blocks)
├─ req_blocks に null_block × num_skipped_blocks を追加
├─ req_blocks に new_computed_blocks を追加
├─ num_cached_block[request_id] = len(req_blocks)
└─ external_computed_tokens > 0 → 追加ブロック割り当て
allocate_new_blocks() [VERIFIED]
参照: L208
def allocate_new_blocks(self, request_id, num_tokens, num_tokens_main_model):
num_required_blocks = cdiv(num_tokens, self.block_size)
num_new_blocks = num_required_blocks - len(req_blocks)
if num_new_blocks <= 0:
return []
new_blocks = self.block_pool.get_new_blocks(num_new_blocks)
req_blocks.extend(new_blocks)
return new_blocks # 新規分のみ返す
cache_blocks() [VERIFIED]
参照: L235
def cache_blocks(self, request, num_tokens):
num_full_blocks = num_tokens // self.block_size
if num_cached_blocks >= num_full_blocks:
return # 既に登録済み
block_pool.cache_full_blocks(request, req_blocks, num_cached_blocks,
num_full_blocks, block_size, group_id)
num_cached_block[request_id] = num_full_blocks
free() [VERIFIED]
参照: L261
def free(self, request_id):
req_blocks = self.req_to_blocks.pop(request_id, [])
ordered_blocks = reversed(req_blocks) # 逆順でLRU最適化
self.block_pool.free_blocks(ordered_blocks)
self.num_cached_block.pop(request_id, None)
逆順の理由: チェーン末尾(最新トークン)がキューの先頭側に来ることで、次の割り当て時に最初に evict される。先頭ブロック(プレフィックス)は長く残り、共有確率が上がる。
remove_skipped_blocks() [VERIFIED]
参照: L343
def remove_skipped_blocks(self, request_id, total_computed_tokens):
num_skipped_tokens = self.get_num_skipped_tokens(total_computed_tokens)
if num_skipped_tokens <= 0:
return # Full Attention: 何もしない
# 後方から走査して null_block に遭遇したら停止
for i in range(num_skipped_blocks - 1, -1, -1):
if blocks[i] == null_block:
break # 前回の呼び出しで既に解放済み
removed_blocks.append(blocks[i])
blocks[i] = null_block
block_pool.free_blocks(removed_blocks)
get_num_skipped_tokens() [VERIFIED]
参照: L386
デフォルト実装は return 0(全トークンがアテンション対象)。サブクラスでオーバーライド。
FullAttentionManager [DEEP] [VERIFIED]
参照: target/vllm/vllm/v1/core/single_type_kv_cache_manager.py:400
標準的な全トークンアテンション。基底クラスの動作をそのまま継承し、2 つのメソッドのみ実装。
find_longest_cache_hit()
参照: L401
左→右にブロックハッシュを走査:
キャッシュヒット → computed_blocks に追加
キャッシュミス → break(チェーンが途切れたら以降は必ずミス)
EAGLE 使用時:
最後のブロックを削除(hidden states 再計算が必要)
alignment_tokens でアライメント調整:
Hybrid モデルで LCM ブロックサイズの倍数に切り詰め
Downward-closed 性質: Full Attention では blocks[0..n] がヒットするなら blocks[0..n-1] も必ずヒットする。この性質により、左→右の貪欲スキャンで最適解が得られる。
get_num_common_prefix_blocks()
参照: L450
def get_num_common_prefix_blocks(self, running_request_id):
for block in blocks:
if block.ref_cnt == len(self.req_to_blocks):
num_common_blocks += 1
else:
break
return num_common_blocks
原理: ref_cnt == 全リクエスト数 なら、そのブロックは全リクエストで共有されている → 共通プレフィックス。Cascade Attention で共通プレフィックスの再計算をスキップするために使用。
SlidingWindowManager [DEEP] [VERIFIED]
参照: target/vllm/vllm/v1/core/single_type_kv_cache_manager.py:461
Sliding Window Attention 用。ウィンドウ外のトークンの KV キャッシュを解放してメモリを節約する。
コンストラクタ
def __init__(self, kv_cache_spec: SlidingWindowSpec, **kwargs):
super().__init__(kv_cache_spec, **kwargs)
self.sliding_window = kv_cache_spec.sliding_window
get_num_skipped_tokens()
参照: L556
def get_num_skipped_tokens(self, num_computed_tokens):
return max(0, num_computed_tokens - self.sliding_window + 1)
例: sliding_window=4, num_computed_tokens=7
Tokens: [0 1 2 3 4 5 6 | 7]
↑ 次に計算するトークン
[4 5 6 7] ← sliding window(サイズ4)
[0 1 2 3] ← skipped(4トークン)
find_longest_cache_hit()
参照: L466
右→左にブロックハッシュを走査:
キャッシュヒット → computed_blocks[i] にセット、連続カウント++
キャッシュミス → 連続カウント = 0(リセット)
連続カウント >= sliding_window_contiguous_blocks:
末尾をトリミングして break
sliding_window_contiguous_blocks = ceil((window - 1) / block_size)
Right-to-left の理由: Sliding Window は最新のトークン付近のブロックが重要。右端から連続ヒットを探すことで、ウィンドウ内の有用なキャッシュを効率的に発見する。
初期値: computed_blocks は null_block で埋められ、ヒットした位置のみ実ブロックで置換される。
制約事項:
- DCP/PCP 非対応 (
assert dcp_world_size == 1) - EAGLE 使用時は
sliding_window_contiguous_blocks += 1
get_num_common_prefix_blocks()
参照: L584
常に 0 を返す。プレフィックスブロックは全て null_block に置換されているため、Cascade Attention は使用不可。
ChunkedLocalAttentionManager [DEEP] [VERIFIED]
参照: target/vllm/vllm/v1/core/single_type_kv_cache_manager.py:594
チャンク境界でアテンションが分割されるモデル用。各チャンク内のトークンのみが互いにアテンションする。
コンストラクタ
def __init__(self, kv_cache_spec: ChunkedLocalAttentionSpec, **kwargs):
super().__init__(kv_cache_spec, **kwargs)
self.attention_chunk_size = kv_cache_spec.attention_chunk_size
get_num_skipped_tokens()
参照: L691
def get_num_skipped_tokens(self, num_computed_tokens):
return (num_computed_tokens // self.attention_chunk_size) * self.attention_chunk_size
例1: chunk_size=8, computed=13 → skipped=8 (チャンク[0,7]全体)
例2: chunk_size=8, computed=8 → skipped=8 (チャンク[0,7]全体)
例3: chunk_size=8, computed=7 → skipped=0 (まだチャンク内)
find_longest_cache_hit()
参照: L599
1. local_attention_start_idx = (max_length // chunk_size) * chunk_size
2. computed_blocks = [null_block] × (start_idx // block_size) ← ウィンドウ外
3. start_idx から max_num_blocks まで左→右スキャン:
ヒット → append、ミス → break
ウィンドウ外のブロックは null_block でパディングし、ウィンドウ内のみ FullAttention と同様の左→右スキャンを行う。
制約事項:
- EAGLE 非対応 (
assert use_eagle is False) - DCP/PCP 非対応
- 異なるブロックサイズの混在非対応
MambaManager [DEEP] [VERIFIED]
参照: target/vllm/vllm/v1/core/single_type_kv_cache_manager.py:744
Mamba(State Space Model / 線形アテンション)用の Manager。Transformer ベースのアテンションとは根本的に異なり、KV キャッシュではなく「状態」を管理する。
2つのキャッシュモード
| モード | 説明 |
|---|---|
none(デフォルト) | 基底クラスの動作 + speculative blocks 追加 |
align | 最後の状態ブロックのみ追跡、null_block パディング、speculative blocks 再利用 |
追加状態(align モード)
| フィールド | 型 | 説明 |
|---|---|---|
last_state_block_idx | dict[str, int] | 前ステップで割り当てた状態ブロックのインデックス |
_allocated_block_reqs | set[str] | ブロック割り当て済みリクエストの集合 |
num_speculative_blocks | int | Speculative Decoding 用の余分なブロック数 |
get_num_skipped_tokens()
参照: L967
def get_num_skipped_tokens(self, num_computed_tokens):
return num_computed_tokens - 1 # 最後の状態のみ必要
Mamba は状態の累積的な更新なので、最後のトークンの状態さえあれば以前のトークンの状態は不要。
find_longest_cache_hit()
参照: L756
右→左に走査:
最初のヒットで即座に break
ヒット位置の前を null_block で埋める
最後の状態のみ必要なため、最右のヒット 1 つで十分。
get_num_blocks_to_allocate()(align モード) [DEEP] [VERIFIED]
参照: L832
align モード:
├─ 既存リクエスト(_allocated_block_reqs に存在):
│ └─ 最大 1 ブロック追加(speculative blocks 再利用のため)
│
└─ 新規リクエスト:
└─ 1 + num_speculative_blocks ブロック
allocate_new_blocks()(align モード) [DEEP] [VERIFIED]
参照: L885
align モードの割り当ては複雑:
1. num_tokens を main model 分に制限(lookahead 除外)
2. last_state_block_idx を記録:
- 既存: prev_len - 1 - num_speculative_blocks
- 新規(キャッシュヒット有): prev_len - 1
3. null_block でスキップ位置をパディング
4. 既存リクエスト: speculative blocks をスキップ位置に移動して再利用
5. 残りの新規ブロックを割り当て
remove_skipped_blocks()(align モード)
参照: L804
基底クラスの remove_skipped_blocks() に加え、last_state_block_idx のブロックも解放する:
if last_state_block_idx < cdiv(num_computed_tokens, block_size) - 1:
block_pool.free_blocks([blocks[last_state_block_idx]])
blocks[last_state_block_idx] = null_block
2 ステップ前のブロックが不要になるタイミングで解放する。
free()
参照: L961
def free(self, request_id):
if self.mamba_cache_mode == "align":
self._allocated_block_reqs.discard(request_id)
self.last_state_block_idx.pop(request_id, None)
super().free(request_id)
CrossAttentionManager [DEEP] [VERIFIED]
参照: target/vllm/vllm/v1/core/single_type_kv_cache_manager.py:976
エンコーダ-デコーダモデル(Whisper 等)のクロスアテンション用。エンコーダ出力はリクエスト固有(異なる音声/画像入力)のため、プレフィックスキャッシュの恩恵がない。
制約
| メソッド | 動作 |
|---|---|
allocate_new_computed_blocks() | assert len(new_computed_blocks) == 0 |
cache_blocks() | raise ValueError |
find_longest_cache_hit() | raise NotImplementedError |
get_num_common_prefix_blocks() | return 0 |
エンコーダブロックはリクエスト開始時に num_encoder_tokens に基づいて静的に割り当てられ、デコード中は変化しない。
SinkFullAttentionManager [DEEP] [VERIFIED]
参照: target/vllm/vllm/v1/core/single_type_kv_cache_manager.py:1025
StreamingLLM のための Attention Sink 実装。FullAttentionManager を継承し、初期化時に先頭の sink ブロックを事前確保する。
コンストラクタ
class SinkFullAttentionManager(FullAttentionManager):
def __init__(self, kv_cache_spec: SinkFullAttentionSpec, ...):
super().__init__(...)
sink_len = kv_cache_spec.sink_len
assert sink_len > 0 and sink_len % self.block_size == 0
num_sink_block = sink_len // self.block_size
self.sink_blocks = self.block_pool.free_block_queue.popleft_n(num_sink_block)
特徴:
sink_lenはblock_sizeの倍数でなければならない- sink ブロックは初期化時に
popleft_n()で確保され、以降解放されない - FullAttentionManager の
find_longest_cache_hit()とget_num_common_prefix_blocks()をそのまま使用
各 Manager の比較表 [VERIFIED]
| Manager | スキップ計算 | キャッシュ検索 | Cascade | DCP/PCP | EAGLE |
|---|---|---|---|---|---|
| FullAttention | 0(全トークン) | 左→右 | ref_cnt 基準 | 対応 | 対応 |
| SlidingWindow | max(0, n-w+1) | 右→左(連続) | 非対応 | 非対応 | 対応 |
| ChunkedLocal | (n//c)*c | null_pad + 左→右 | 非対応 | 非対応 | 非対応 |
| Mamba | n - 1 | 右→左(単一) | 非対応 | 非対応 | - |
| CrossAttention | 0 | 非対応 | 非対応 | - | - |
| SinkFullAttention | 0 | 左→右 | ref_cnt 基準 | 対応 | 対応 |
関連ドキュメント
BlockPool 詳細
深度: [DEEP] 確信度: [VERIFIED] 最終更新: 2026-02-11
概要
BlockPool はKVキャッシュの物理ブロックを管理するクラスである。ブロックの割り当て・解放・プレフィックスキャッシュ索引を一元管理し、LRU Eviction によるメモリ再利用を実現する。3つの内部データ構造(FreeKVCacheBlockQueue、BlockHashToBlockMap、KVCacheBlock)で構成される。
参照: target/vllm/vllm/v1/core/block_pool.py:128
KVCacheBlock [DEEP] [VERIFIED]
KVキャッシュブロック1つのメタデータを保持する dataclass。物理メモリ自体は GPU 上にあり、このオブジェクトは CPU 側のメタデータのみを管理する。
参照: target/vllm/vllm/v1/core/kv_cache_utils.py:107
フィールド
| フィールド | 型 | 説明 |
|---|---|---|
block_id | int | 0 〜 num_gpu_blocks - 1 の一意識別子 |
ref_cnt | int | 参照カウント。0なら空きキュー内(Eviction候補) |
_block_hash | BlockHashWithGroupId | None | プレフィックスキャッシュ用ハッシュキー。fullブロックでキャッシュ登録済みの場合のみ設定 |
prev_free_block | KVCacheBlock | None | 空きキューの前ノードポインタ |
next_free_block | KVCacheBlock | None | 空きキューの次ノードポインタ |
is_null | bool | null_block フラグ。True の場合は解放・Eviction 対象外 |
block_hash プロパティ
@block_hash.setter
def block_hash(self, block_hash: BlockHashWithGroupId):
assert self.block_hash is None # 二重設定を禁止
self._block_hash = block_hash
def reset_hash(self):
self._block_hash = None # Eviction時にリセット
制約: setter は assert self.block_hash is None で二重設定を防止する。ハッシュのリセットは reset_hash() のみで行う。これによりブロックのライフサイクルが「未設定 → 設定 → リセット → 再設定」の順序で制御される。
ライフサイクル
1. 生成: BlockPool.__init__() で全ブロック生成(ref_cnt=0)
2. 割り当て: get_new_blocks() → ref_cnt=1
3. キャッシュ登録: cache_full_blocks() → block_hash 設定
4. 再利用(キャッシュヒット): touch() → ref_cnt++
5. 解放: free_blocks() → ref_cnt-- → 0なら空きキューへ
6. Eviction: _maybe_evict_cached_block() → hash リセット → 再割り当て
FreeKVCacheBlockQueue [DEEP] [VERIFIED]
空きブロックを LRU 順序で管理する双方向リンクリスト。Python 組み込みの deque ではなく独自実装を採用している。
参照: target/vllm/vllm/v1/core/kv_cache_utils.py:156
なぜ独自実装か
deque では中間要素の削除が O(n) であるのに対し、この実装では O(1) で削除できる。touch() でキャッシュヒットしたブロックを空きキューの中間から即座に除去する必要があるため、O(1) の remove() が不可欠である。
また、Python オブジェクトのアロケーションを行わず、KVCacheBlock の prev_free_block/next_free_block ポインタを直接操作するため、GC 負荷が低い。
センチネルノード
fake_head ⇄ block_0 ⇄ block_1 ⇄ ... ⇄ block_n ⇄ fake_tail
fake_free_list_head:block_id=-1のダミーノード。先頭の前に配置fake_free_list_tail:block_id=-1のダミーノード。末尾の後に配置- 目的: null チェックの分岐を減らし、コードを簡素化
操作一覧
| メソッド | 行 | 計算量 | 説明 |
|---|---|---|---|
popleft() | L208 | O(1) | 先頭ブロックを取り出し |
popleft_n(n) | L245 | O(n) | 先頭から n 個を一括取り出し |
remove(block) | L278 | O(1) | 中間のブロックを除去(touch 用) |
append(block) | L298 | O(1) | 末尾にブロックを追加 |
append_n(blocks) | L321 | O(n) | 末尾に複数ブロックを一括追加 |
get_all_free_blocks() | L346 | O(m) | 全空きブロック取得(テスト用) |
LRU 順序の維持
- 初期状態:
block_id順(0, 1, 2, …) - 再挿入時:
free_blocks()でブロックが返却される際に逆順で追加される- 理由: リクエストのブロックチェーンの末尾(最新トークン)は先に evict されるべき。先頭(古いプレフィックス)は他のリクエストと共有される可能性が高いため後回し
- 逆順操作は
SingleTypeKVCacheManager.free()側で実行される(BlockPool 外)
BlockHashToBlockMap [DEEP] [VERIFIED]
プレフィックスキャッシュのハッシュ→ブロック対応表。
参照: target/vllm/vllm/v1/core/block_pool.py:32
データ構造
_cache: dict[BlockHashWithGroupId, KVCacheBlock | dict[int, KVCacheBlock]]
Union 型の最適化: 大半のハッシュキーには 1 ブロックしか対応しないため、単一ブロックは直接格納し、2つ以上の場合のみ内部 dict に昇格する。これにより内部 dict の GC コストを削減。
重複排除なし設計
同一ハッシュのブロックが複数存在しても重複排除しない。理由: ブロック ID をリクエストに割り当てた後は追加のみ(append-only)を保証するため。重複排除するとブロック ID が変わり、ブロックテーブルの安定性が崩れる。
操作
| メソッド | 行 | 説明 |
|---|---|---|
get_one_block(key) | L60 | ハッシュキーに対応する任意の1ブロックを返す。複数あれば先頭 |
insert(key, block) | L73 | ブロックをキャッシュに追加。1→dict 昇格を自動処理 |
pop(key, block_id) | L91 | 特定の block_id を除去。残りがあれば dict を復元 |
__len__() | L121 | ハッシュキー数(ブロック数ではない) |
insert の分岐
key なし → _cache[key] = block (単一格納)
key に 1 block → _cache[key] = {id: blk, ...} (dict 昇格)
key に dict → dict[block.block_id] = block (dict 追加)
null_block [DEEP] [VERIFIED]
参照: target/vllm/vllm/v1/core/block_pool.py:174
特性
- block_id = 0: 初期化時に空きキューから最初に popleft される
- is_null = True: 解放・Eviction の対象外
- ref_cnt 未管理:
touch()やfree_blocks()で特別にスキップされる
用途
| 用途 | 説明 |
|---|---|
| Sliding Window Attention | ウィンドウ外のブロック位置を埋める。物理メモリを消費しない |
| Mamba (align モード) | スキップされたブロック位置のパディング |
| ブロックテーブルの長さ統一 | Attention カーネルにはブロックテーブルの連続性が必要。null_block で長さを揃える |
ガード条件
# touch() (L383)
if block.ref_cnt == 0 and not block.is_null:
self.free_block_queue.remove(block)
# free_blocks() (L401-402)
[block for block in blocks_list if block.ref_cnt == 0 and not block.is_null]
# cache_full_blocks() (L260-261)
if blk.is_null:
continue # null ブロックはキャッシュしない
ブロック割り当てフロー [DEEP] [VERIFIED]
get_new_blocks()
参照: target/vllm/vllm/v1/core/block_pool.py:300
get_new_blocks(num_blocks)
│
├─ 空きブロック数チェック → 不足なら ValueError
│
├─ free_block_queue.popleft_n(num_blocks)
│ └─ LRU 先頭(最古)から取り出し
│
├─ enable_caching の場合:
│ ├─ _maybe_evict_cached_block(block) ← ハッシュクリア
│ ├─ assert block.ref_cnt == 0
│ ├─ block.ref_cnt += 1
│ └─ metrics_collector.on_block_allocated(block) ← サンプリング
│
└─ enable_caching でない場合:
├─ assert block.ref_cnt == 0
├─ block.ref_cnt += 1
└─ metrics_collector.on_block_allocated(block)
注意: この関数はキャッシュ検索を行わない。キャッシュヒットの確認は get_cached_block() で別途行う。
get_cached_block()
参照: target/vllm/vllm/v1/core/block_pool.py:182
get_cached_block(block_hash, kv_cache_group_ids)
│
├─ 各 group_id について:
│ ├─ make_block_hash_with_group_id(block_hash, group_id)
│ ├─ cached_block_hash_to_block.get_one_block(hash_with_id)
│ └─ 1つでも miss → None を返す(全グループ一致が必須)
│
└─ 全ヒット → list[KVCacheBlock] を返す
All-or-nothing セマンティクス: 複数の KV キャッシュグループがある場合、全グループでヒットしなければキャッシュミスとして扱う。
ブロック解放フロー [DEEP] [VERIFIED]
free_blocks()
参照: target/vllm/vllm/v1/core/block_pool.py:389
def free_blocks(self, ordered_blocks: Iterable[KVCacheBlock]) -> None:
blocks_list = list(ordered_blocks) # イテレータを実体化
for block in blocks_list:
block.ref_cnt -= 1 # 参照カウント減少
self.free_block_queue.append_n(
[block for block in blocks_list
if block.ref_cnt == 0 and not block.is_null] # 0 到達 & 非 null のみ
)
逆順解放の理由: 呼び出し元(SingleTypeKVCacheManager.free())がブロックを逆順にして渡す。チェーン末尾(最新トークン)がキューの先頭側に来るため、次回の get_new_blocks() で最初に evict される。先頭ブロック(プレフィックス)は末尾側に来るため、プレフィックスキャッシュとして長く生き残る。
touch()
参照: target/vllm/vllm/v1/core/block_pool.py:372
プレフィックスキャッシュヒット時に呼ばれ、ブロックの再利用を記録する。
touch(blocks)
│
├─ ref_cnt == 0 かつ非 null の場合:
│ └─ free_block_queue.remove(block) ← 空きキューから除去
│
├─ ref_cnt += 1
│
└─ metrics_collector.on_block_accessed(block)
Eviction メカニズム [DEEP] [VERIFIED]
_maybe_evict_cached_block()
参照: target/vllm/vllm/v1/core/block_pool.py:332
新規ブロック割り当て時に、そのブロックがプレフィックスキャッシュに登録されている場合にキャッシュから除去する。
_maybe_evict_cached_block(block)
│
├─ metrics_collector.on_block_evicted(block) ← 先にメトリクス記録
│
├─ block.block_hash is None → return False(キャッシュ未登録)
│
├─ cached_block_hash_to_block.pop(hash, block_id)
│ └─ None → return False(マップに不在)
│
├─ block.reset_hash() ← ハッシュをクリア
│
├─ enable_kv_cache_events の場合:
│ └─ kv_event_queue.append(BlockRemoved(...))
│
└─ return True
タイミング: get_new_blocks() 内で ref_cnt をインクリメントする前に呼ばれる。
evict_blocks()
参照: target/vllm/vllm/v1/core/block_pool.py:405
外部(KV コネクタ)から特定の block_id 群を明示的に evict する。_maybe_evict_cached_block() を各ブロックに対して呼ぶ。ブロックは空きキューからは除去しない(ハッシュの除去のみ)。
reset_prefix_cache()
参照: target/vllm/vllm/v1/core/block_pool.py:424
全プレフィックスキャッシュのリセット。RLHF でモデル重み更新後にキャッシュを無効化する用途。
前提条件: 使用中のブロックが null_block のみ(num_used_blocks == 1)。条件を満たさない場合は False を返して失敗。
reset_prefix_cache()
├─ 使用中ブロック数 != 1 → return False
├─ cached_block_hash_to_block を新規インスタンスで置換
├─ 全ブロックの hash をリセット
├─ metrics_collector.reset()
├─ kv_event_queue.append(AllBlocksCleared())
└─ return True
KV Cache Events [DEEP] [VERIFIED]
参照: target/vllm/vllm/v1/core/block_pool.py:177-178, 480-490
KV Transfer(Disaggregated Prefill)連携のためのイベントシステム。
| イベント型 | 発行タイミング | 用途 |
|---|---|---|
BlockStored | cache_full_blocks() | 新規ブロックがキャッシュ登録された |
BlockRemoved | _maybe_evict_cached_block() | ブロックがキャッシュから除去された |
AllBlocksCleared | reset_prefix_cache() | 全キャッシュがリセットされた |
def take_events(self) -> list[KVCacheEvent]:
"""アトミックにイベントキューを排出"""
events = self.kv_event_queue
self.kv_event_queue = [] # 新規リストで置換(参照スワップ)
return events
キャッシュ使用率 [VERIFIED]
参照: target/vllm/vllm/v1/core/block_pool.py:467
def get_usage(self) -> float:
total_gpu_blocks = self.num_gpu_blocks - 1 # null_block を除外
return 1.0 - (self.get_num_free_blocks() / total_gpu_blocks)
null_block は常に「使用中」だが、使用率の計算からは除外される。
メトリクス収集 [DEEP] [VERIFIED]
参照: target/vllm/vllm/v1/core/kv_cache_metrics.py:46
KVCacheMetricsCollector はサンプリングベースのブロック滞留メトリクスを収集する。
BlockMetricsState
参照: target/vllm/vllm/v1/core/kv_cache_metrics.py:16
個別ブロックのライフサイクル指標:
| フィールド | 説明 |
|---|---|
birth_time_ns | 割り当て時刻(time.monotonic_ns()) |
last_access_ns | 最終アクセス時刻 |
access_history | アクセス履歴(最大4件、deque(maxlen=4)) |
サンプリング
sample_rate: float = 0.01 # デフォルト1%
def should_sample_block(self) -> bool:
return random.random() < self.sample_rate
全ブロックを追跡するとオーバーヘッドが大きいため、割り当て時に確率的にサンプリングする。サンプリングされたブロックのみ BlockMetricsState が生成される。
イベントフック
| フック | タイミング | 処理 |
|---|---|---|
on_block_allocated(block) | get_new_blocks() | サンプル判定、BlockMetricsState 生成 |
on_block_accessed(block) | touch() | record_access() 呼び出し |
on_block_evicted(block) | _maybe_evict_cached_block() | KVCacheEvictionEvent を生成・蓄積 |
Eviction 時に生成される KVCacheEvictionEvent には lifetime_seconds、idle_seconds、reuse_gaps_seconds が含まれ、drain_events() で一括取得できる。
BlockPool 初期化 [VERIFIED]
参照: target/vllm/vllm/v1/core/block_pool.py:147
def __init__(self, num_gpu_blocks, enable_caching, hash_block_size,
enable_kv_cache_events=False, metrics_collector=None):
self.blocks = [KVCacheBlock(idx) for idx in range(num_gpu_blocks)]
self.free_block_queue = FreeKVCacheBlockQueue(self.blocks)
self.cached_block_hash_to_block = BlockHashToBlockMap()
self.null_block = self.free_block_queue.popleft() # block_id=0
self.null_block.is_null = True
hash_block_size: ハッシュ計算に使うブロックサイズ。通常は実際のブロックサイズと一致するが、Hybrid モデル(異なるブロックサイズの KV キャッシュグループ)では異なる場合があるenable_kv_cache_events: KV Transfer 連携用のイベント発行を有効化metrics_collector: サンプリングベースの滞留メトリクス収集(オプション)
関連ドキュメント
プレフィックスキャッシュ詳細
深度: [DEEP] 確信度: [VERIFIED] 最終更新: 2026-02-11
概要
プレフィックスキャッシュは、異なるリクエスト間で共通するプロンプトプレフィックスの KV キャッシュブロックを再利用する機構である。トークン列をブロック単位でハッシュ化し、ハッシュチェーン(各ブロックのハッシュが前のブロックのハッシュに依存)を構築することで、プレフィックスの最長一致を効率的に検索する。
ハッシュチェーン計算 [DEEP] [VERIFIED]
hash_block_tokens()
参照: target/vllm/vllm/v1/core/kv_cache_utils.py:525
各ブロックのハッシュは 3 要素のタプルから計算される:
def hash_block_tokens(hash_function, parent_block_hash, curr_block_token_ids,
extra_keys=None) -> BlockHash:
if not parent_block_hash:
parent_block_hash = NONE_HASH # 先頭ブロック用のシード
curr_block_token_ids_tuple = tuple(curr_block_token_ids)
return BlockHash(
hash_function((parent_block_hash, curr_block_token_ids_tuple, extra_keys))
)
ハッシュ入力の 3 要素:
| 要素 | 説明 |
|---|---|
parent_block_hash | 前ブロックのハッシュ(先頭ブロックは NONE_HASH) |
curr_block_token_ids_tuple | 現ブロックのトークン ID 列(tuple 化) |
extra_keys | LoRA、マルチモーダル、cache_salt、prompt_embeds(後述) |
チェーン依存性
Block 0: hash(NONE_HASH, tokens[0:B], extra) → H0
Block 1: hash(H0, tokens[B:2B], extra) → H1
Block 2: hash(H1, tokens[2B:3B], extra) → H2
...
なぜチェーンか: 各ハッシュが全ての先行ブロックに依存するため、プレフィックスが異なれば後続のハッシュも必ず異なる。これにより左から右へのスキャンで「最初のミスで停止」すれば最長プレフィックス一致が得られる。
NONE_HASH
参照: target/vllm/vllm/v1/core/kv_cache_utils.py:77
チェーンの起点となるシード値:
def init_none_hash(hash_fn):
hash_seed = os.getenv("PYTHONHASHSEED")
if hash_seed is None:
NONE_HASH = BlockHash(os.urandom(32)) # ランダム 32 バイト
else:
NONE_HASH = BlockHash(hash_fn(hash_seed)) # 決定論的
- PYTHONHASHSEED 未設定: ランダムシード → プロセス間でハッシュが一致しない
- PYTHONHASHSEED 設定済み: 決定論的 → プロセス間でハッシュを共有可能(KV Transfer で必要)
- CBOR ベースのハッシュ関数で PYTHONHASHSEED 未設定の場合は警告が出る
BlockHash 型 [DEEP] [VERIFIED]
参照: target/vllm/vllm/v1/core/kv_cache_utils.py:34
型階層
| 型 | 定義 | 用途 |
|---|---|---|
BlockHash | NewType("BlockHash", bytes) | ブロック単体のハッシュ値 |
BlockHashWithGroupId | NewType("BlockHashWithGroupId", bytes) | ハッシュ + KV キャッシュグループ ID(4 バイト BE) |
ExternalBlockHash | bytes | int | 外部向けハッシュ(後方互換性のための Union) |
BlockHashWithGroupId のパッキング
def make_block_hash_with_group_id(block_hash, group_id):
return BlockHashWithGroupId(
block_hash + group_id.to_bytes(4, "big", signed=False)
)
def get_block_hash(key): return BlockHash(key[:-4])
def get_group_id(key): return int.from_bytes(key[-4:], "big")
設計: tuple ではなく bytes 結合でパッキングすることで、Python オブジェクト生成を回避し、GC 負荷を低減。
ハッシュ関数 [DEEP] [VERIFIED]
参照: target/vllm/vllm/utils/hashing.py
4 種類のハッシュ関数が利用可能:
| 名前 | シリアライゼーション | ハッシュ | 出力サイズ | 特徴 |
|---|---|---|---|---|
sha256 | pickle | SHA-256 | 32 bytes | Python 依存 |
sha256_cbor | CBOR (canonical) | SHA-256 | 32 bytes | デフォルト。言語非依存・再現可能 |
xxhash | pickle | xxh3_128 | 16 bytes | 高速、Python 依存 |
xxhash_cbor | CBOR (canonical) | xxh3_128 | 16 bytes | 高速、言語非依存 |
デフォルト: sha256_cbor — CBOR の canonical モードにより、PYTHONHASHSEED に依存しないシリアライゼーションが可能。プロセス間でハッシュを共有する KV Transfer に適している。
設定: vllm_config.cache_config.prefix_caching_hash_algo
Extra Keys [DEEP] [VERIFIED]
参照: target/vllm/vllm/v1/core/kv_cache_utils.py:367
同一トークン列でも異なる KV キャッシュを持つ場合に、追加情報をハッシュに含める。
必要判定
def need_extra_keys(request):
return (bool(request.mm_features) # マルチモーダル
or (request.lora_request is not None) # LoRA
or (request.cache_salt is not None)) # キャッシュソルト
各 Extra Key の生成
| 生成関数 | 行 | 内容 | 適用範囲 |
|---|---|---|---|
_gen_mm_extra_hash_keys() | L387 | MM 入力の identifier | ブロックと重なる MM 入力のみ |
_gen_lora_extra_hash_keys() | L451 | LoRA アダプタの lora_name | 全ブロック共通 |
cache_salt | L508-509 | ユーザー指定のキャッシュソルト | 先頭ブロックのみ (start_token_idx == 0) |
_gen_prompt_embeds_extra_hash_keys() | L466 | プロンプト埋め込みの生テンソルバイト | ブロック範囲分のスライス |
結合順序
extra_keys = lora_extra_keys + mm_extra_keys + cache_salt_keys + prompt_embeds_keys
空の場合は None を返し、ハッシュ計算の extra_keys 引数として渡される。
マルチモーダル Extra Keys の詳細
参照: target/vllm/vllm/v1/core/kv_cache_utils.py:387
_gen_mm_extra_hash_keys() はブロックの [start, end) トークン範囲と MM 入力の [offset, offset+length) 範囲の重なりを検出する:
MM入力: [offset ─────── offset+length]
Block: [start ──── end]
↑ 重なりあり → identifier を extra_keys に追加
mm_featuresはmm_position.offsetでソート済みと仮定start_mm_idxで走査位置を追跡し、毎回先頭から検索しないstart_mm_idx = -1は「最後の MM 入力」を示す(生成トークンが増えるデコードフェーズで使用)
リクエストブロックハッシャー [DEEP] [VERIFIED]
参照: target/vllm/vllm/v1/core/kv_cache_utils.py:555
ファクトリ関数
def get_request_block_hasher(block_size, caching_hash_fn):
def request_block_hasher(request) -> list[BlockHash]:
start_token_idx = len(request.block_hashes) * block_size
# full ブロックのみハッシュ(不完全ブロックはスキップ)
if start_token_idx + block_size > request.num_tokens:
return []
# ...ハッシュチェーンを走査して新規 full ブロックのハッシュを計算
return request_block_hasher
遅延・インクリメンタル計算
- 初期化時:
Request.__init__()でblock_hasherが渡された場合、即座にget_hash_new_full_blocks()を呼び、プロンプトの full ブロック分のハッシュを計算 - トークン追加時:
Request.append_output_token_ids()で新トークンが追加されるたびにget_hash_new_full_blocks()を呼び、新たに full になったブロックのハッシュをインクリメンタルに追加
# Request.__init__()
self.block_hashes = self.get_hash_new_full_blocks() # 初期ハッシュ
# Request.append_output_token_ids()
self.block_hashes.extend(self.get_hash_new_full_blocks()) # 増分追加
制約: 不完全ブロック(最後のブロックが block_size 未満)はハッシュされない。これによりプレフィックスキャッシュは常にブロック境界単位で一致する。
チェーンの継続
prev_block_hash_value = request.block_hashes[-1] if request.block_hashes else None
前回計算済みの最後のハッシュを parent_block_hash として使い、チェーンを継続する。
BlockHashListWithBlockSize [DEEP] [VERIFIED]
参照: target/vllm/vllm/v1/core/kv_cache_utils.py:1571
Hybrid モデル(異なるブロックサイズの KV キャッシュグループ)でハッシュ粒度を変換するアダプタ。
動作原理
hash_block_size = 16, target_block_size = 32 の場合:
元のハッシュ: [H0, H1, H2, H3] (各16トークン)
変換後: [H0+H1, H2+H3] (各32トークン)
↑ bytes 結合
def _get_value_at(self, idx):
base = idx * self.scale_factor # scale_factor = target / hash
end = base + self.scale_factor
merged_hash = self.block_hashes[base]
for i in range(base + 1, end):
merged_hash += self.block_hashes[i] # bytes 結合
return BlockHash(merged_hash)
遅延評価: アクセス時にのみ変換を実行。__getitem__、__iter__、__len__ をサポートし、通常の list[BlockHash] と同じインターフェースで使える。
Lookup アルゴリズム [DEEP] [VERIFIED]
プレフィックスキャッシュの検索は SingleTypeKVCacheManager のサブクラスごとに異なるアルゴリズムを持つ。
FullAttentionManager: 左→右スキャン
参照: target/vllm/vllm/v1/core/single_type_kv_cache_manager.py:401
for block_hash in block_hashes[:max_num_blocks]:
if cache hit:
computed_blocks.append(cached_block)
else:
break ← 最初のミスで停止
- Downward-closed 性質: blocks[0..n] がヒットするなら blocks[0..n-1] も必ずヒットする
- EAGLE 使用時は最後のブロックを削除(hidden states が必要なため)
alignment_tokensでアライメント調整(Hybrid モデルでの LCM ブロックサイズ)
SlidingWindowManager: 右→左スキャン
参照: target/vllm/vllm/v1/core/single_type_kv_cache_manager.py:466
sliding_window_contiguous_blocks = ceil((sliding_window - 1) / block_size)
for i in range(max_num_blocks - 1, -1, -1): # 右→左
if cache hit:
computed_blocks[i] = cached_block
num_contiguous_blocks += 1
if num_contiguous_blocks >= required:
break ← ウィンドウ分の連続ブロック確保
else:
num_contiguous_blocks = 0 ← 連続性リセット
- 連続ブロックが必須: Sliding Window Attention はウィンドウ内の連続したトークンにのみアテンションするため、不連続なブロックは使えない
computed_blocksは初期値としてnull_blockで埋められ、ヒットした位置のみ実ブロックで置換される- アライメントチェック: 右端のブロックがアライメント境界に合わない場合はスキップ
ChunkedLocalAttentionManager
参照: target/vllm/vllm/v1/core/single_type_kv_cache_manager.py:594
チャンク境界でアテンションが分割されるモデル用。ウィンドウ外のブロックは null_block でパディングし、ウィンドウ内のみ左→右スキャンで検索する。
MambaManager: 右→左、単一一致
参照: target/vllm/vllm/v1/core/single_type_kv_cache_manager.py:744
Mamba(線形アテンション)は最後の状態のみが必要なため、最初のヒットで即座に停止する。
HybridKVCacheCoordinator: 反復固定点
参照: target/vllm/vllm/v1/core/kv_cache_coordinator.py:448
複数のアテンションタイプが混在する Hybrid モデルでは、各グループの最長ヒット長が相互に制約し合う:
hit_length = max_cache_hit_length
while True:
curr_hit_length = hit_length
for each attention_group:
if is_full_attention and cached:
# Downward-closed: 既存結果を再利用
curr_hit_length = (curr_hit_length // block_size) * block_size
else:
hit = find_longest_cache_hit(curr_hit_length)
curr_hit_length = len(hit) * block_size
if curr_hit_length >= hit_length:
break ← 収束(もう減らない)
hit_length = curr_hit_length
if is_simple_hybrid: # FullAttn + 1種のみ
break ← 1回で十分
収束保証: hit_length は単調減少するため、有限回で収束する。
最適化: Full Attention は downward-closed なので、他グループの結果に合わせてカットするだけでよい。2グループの simple hybrid ケースでは 1 イテレーションで確定。
キャッシュ登録 [DEEP] [VERIFIED]
cache_full_blocks()
参照: target/vllm/vllm/v1/core/block_pool.py:209
計算済みの full ブロックをプレフィックスキャッシュに登録する。
cache_full_blocks(request, blocks, num_cached_blocks, num_full_blocks, ...)
│
├─ num_cached_blocks >= num_full_blocks → return(既に登録済み)
│
├─ new_full_blocks = blocks[num_cached_blocks:num_full_blocks]
│
├─ block_size == hash_block_size の場合:
│ └─ block_hashes = request.block_hashes(直接使用)
│
├─ block_size != hash_block_size の場合:
│ └─ block_hashes = BlockHashListWithBlockSize(...)(粒度変換)
│
└─ 各 new_full_block について:
├─ is_null → skip
├─ assert block_hash is None(二重登録防止)
├─ block_hash_with_group_id を生成
├─ blk.block_hash = block_hash_with_group_id
├─ cached_block_hash_to_block.insert(...)
└─ enable_kv_cache_events → BlockStored イベント発行
num_cached_block トラッキング
SingleTypeKVCacheManager が num_cached_block[request_id] で各リクエストの登録済みブロック数を追跡する。cache_blocks() 呼び出し時に num_cached_blocks >= num_full_blocks なら何もせず、新たに full になったブロックのみを登録する。
データフロー全体 [VERIFIED]
EngineCore.__init__()
└─ init_none_hash(hash_fn) ← グローバル NONE_HASH 初期化
└─ get_request_block_hasher(...) ← ハッシャークロージャ生成
Request.__init__(block_hasher=...)
└─ block_hashes = get_hash_new_full_blocks() ← プロンプトの full ブロックを即時ハッシュ
Request.append_output_token_ids()
└─ block_hashes.extend(get_hash_new_full_blocks()) ← 増分ハッシュ
Scheduler.schedule()
├─ kv_cache_manager.get_computed_blocks(request)
│ └─ coordinator.find_longest_cache_hit(request.block_hashes, max_length)
│ └─ block_pool.get_cached_block(hash, group_ids)
│ └─ BlockHashToBlockMap.get_one_block(hash_with_group_id)
│
└─ kv_cache_manager.allocate_slots(request, ...)
└─ coordinator.cache_blocks(request, num_tokens_to_cache)
└─ block_pool.cache_full_blocks(request, blocks, ...)
└─ BlockHashToBlockMap.insert(hash_with_group_id, block)
関連ドキュメント
KV Transfer [MEDIUM] [VERIFIED]
最終更新: 2026-02-15 対象ソース:
target/vllm/vllm/distributed/kv_transfer/,target/vllm/vllm/v1/worker/kv_connector_model_runner_mixin.py
概要
KV Transferは、vLLMインスタンス間またはストレージ間でデコーダKVキャッシュを転送するためのプラグインフレームワーク。Disaggregated Prefill(P/D分離)、KVキャッシュのオフロード、外部キャッシュ(LMCache等)との連携を実現する。
ECConnector(ec_transfer/)とは完全に独立した系統。ECConnectorがエンコーダキャッシュ専用であるのに対し、KV Transferはデコーダ側のKVキャッシュのみを対象とする。ただし、設計パターン(2ロール分離、Factory、Mixin統合)は共通している。
アーキテクチャ
graph TD
subgraph Scheduler["Scheduler プロセス"]
S[Scheduler] -->|"get_num_new_matched_tokens()"| KC_S[KVConnector<br/>SCHEDULER ロール]
S -->|"update_state_after_alloc()"| KC_S
S -->|"build_connector_meta()"| KC_S
S -->|"request_finished()"| KC_S
S -->|"take_events()"| KC_S
end
subgraph Worker["Worker プロセス"]
MR[GPUModelRunner] -->|"Mixin"| KC_W[KVConnector<br/>WORKER ロール]
KC_W -->|"start_load_kv()"| EXT[外部ストレージ<br/>LMCache / NIXL / etc.]
KC_W -->|"save_kv_layer()"| EXT
ATT[Attention層] -->|"wait_for_layer_load()"| KC_W
ATT -->|"save_kv_layer()"| KC_W
end
KC_S -.->|"KVConnectorMetadata<br/>(via SchedulerOutput)"| KC_W
KC_W -.->|"KVConnectorOutput<br/>(via ModelRunnerOutput)"| KC_S
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/base.py:147 (KVConnectorBase_V1)
2ロール分離
KVConnectorBase_V1は同一クラスだが、SchedulerプロセスとWorkerプロセスで別インスタンスが生成される。KVConnectorFactory.create_connector()がロールを引数に取り、各プロセスで適切なインスタンスを構築する。
# KVConnectorRole enum
class KVConnectorRole(enum.Enum):
SCHEDULER = 0 # Schedulerプロセス内
WORKER = 1 # Workerプロセス内
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/base.py:121
グローバル状態管理
Worker側のKVConnectorインスタンスはグローバル変数で管理される。
| 関数 | 用途 |
|---|---|
get_kv_transfer_group() | 現在のコネクタインスタンス取得(assertで非None保証) |
has_kv_transfer_group() | コネクタ初期化済みか確認 |
ensure_kv_transfer_initialized() | 未初期化なら初期化(is_kv_transfer_instance=True時のみ) |
ensure_kv_transfer_shutdown() | コネクタ停止・グローバル変数クリア |
参照: target/vllm/vllm/distributed/kv_transfer/kv_transfer_state.py:16-74
KVConnectorBase_V1 抽象基底クラス
Abstract メソッド(7つ)
Worker側(4つ)
| メソッド | シグネチャ | 呼び出し元 | 用途 |
|---|---|---|---|
start_load_kv() | (ForwardContext, **kwargs) → None | Mixin(forward前) | KVキャッシュの非同期ロード開始 |
wait_for_layer_load() | (layer_name: str) → None | Attention層内 | レイヤー別ロード完了待機 |
save_kv_layer() | (layer_name, kv_layer, attn_metadata, **kwargs) → None | Attention層内 | レイヤー別KVの非同期セーブ開始 |
wait_for_save() | () → None | Mixin(forward後) | 全セーブ完了待機 |
レイヤーバイレイヤー・パイプライニング: start_load_kv()で全レイヤーのロードを非同期開始し、各Attention層のforward内でwait_for_layer_load()を呼ぶことで、前のレイヤーの計算中に次レイヤーのKVをロードできる。セーブもsave_kv_layer()で即座に非同期開始し、wait_for_save()で全体の完了を保証する。
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/base.py:275-338
Scheduler側(3つ)
| メソッド | シグネチャ | 呼び出し元 | 用途 |
|---|---|---|---|
get_num_new_matched_tokens() | (Request, int) → (int|None, bool) | Scheduler._schedule_waiting() | 外部KVキャッシュで利用可能なトークン数を返す |
update_state_after_alloc() | (Request, KVCacheBlocks, int) → None | Scheduler._schedule_waiting() | ブロック割り当て後のコネクタ状態更新 |
build_connector_meta() | (SchedulerOutput) → KVConnectorMetadata | Scheduler.schedule() | ステップ用メタデータ構築(状態リセットを伴う) |
get_num_new_matched_tokens() の戻り値:
(N, False)— N個の外部トークンが同期的に利用可能(N, True)— N個の外部トークンが非同期ロード(WAITING_FOR_REMOTE_KVS状態へ遷移)(None, _)— まだ判定不能(次回再問い合わせ)(0, False)— 外部キャッシュなし
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/base.py:416-485
非Abstract 重要メソッド
| メソッド | 用途 | デフォルト動作 |
|---|---|---|
bind_connector_metadata() | Scheduler→Workerメタデータ設定 | _connector_metadataに保存 |
clear_connector_metadata() | メタデータクリア | Noneに設定 |
register_kv_caches() | KVキャッシュテンソル事前登録(NIXL等で必要) | no-op |
register_cross_layers_kv_cache() | 全レイヤー一括KVテンソル登録 | no-op |
set_host_xfer_buffer_ops() | ホスト↔デバイス間コピー操作設定 | no-op |
handle_preemptions() | プリエンプション通知(ブロック上書き前) | no-op |
get_finished() | 非同期転送完了リクエストID取得 | (None, None) |
get_block_ids_with_load_errors() | ロード失敗ブロックID取得 | set() |
request_finished() | リクエスト完了通知・遅延解放制御 | (False, None) |
take_events() | KVキャッシュイベント取得 | 空 |
update_connector_output() | Worker出力でScheduler状態更新 | no-op |
補助クラス・インタフェース
| クラス | 用途 |
|---|---|
KVConnectorMetadata (ABC) | Scheduler→Worker間通信メタデータ |
KVConnectorHandshakeMetadata (ABC) | P/Dワーカー間帯域外ハンドシェイク |
SupportsHMA (ABC) | Hybrid Memory Allocator対応インタフェース |
CopyBlocksOp (Callable) | (s_tensors, d_tensors, s_indices, d_indices, direction) → None |
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/base.py:82-144
Cross-Layer Blocks
prefer_cross_layer_blocksプロパティがTrueのコネクタは、全レイヤーのKVを1つの連続テンソルにまとめたレイアウトを使用する。これにより、ブロック単位で全レイヤーのKVデータを一括転送でき、転送効率が向上する。
参照: target/vllm/vllm/v1/worker/kv_connector_model_runner_mixin.py:113-177
KVConnectorFactory
遅延ロードパターンのファクトリ。module_path + class_nameを登録し、使用時にimportする。
登録済みコネクタ(10個)
| 名前 | 用途 | 特徴 |
|---|---|---|
ExampleConnector | デバッグ用 | safetensorsでディスク保存 |
LMCacheConnectorV1 | LMCache統合 | チャンク単位KV保存・3層ストレージ |
LMCacheMPConnector | LMCacheマルチプロセス版 | 別プロセスでLMCache実行 |
NixlConnector | NIXL (RDMA) | 高速GPU間転送 |
P2pNcclConnector | P2P NCCL | NCCL経由の直接GPU転送 |
OffloadingConnector | KVオフロード | CPU/ディスクへのオフロード |
MultiConnector | 複合コネクタ | 複数バックエンドを束ねる |
MoRIIOConnector | MORIIO | MORIIOフレームワーク |
MooncakeConnector | Mooncake | 分散学習フレームワーク |
DecodeBenchConnector | ベンチマーク用 | デコード性能測定 |
動的ロード: kv_connector_module_path設定で未登録クラスも使用可能。旧2引数シグネチャとの互換性チェック(supports_kw())あり。
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/factory.py:27-203
Scheduler統合
外部キャッシュ問い合わせフロー
WAITINGリクエストのスケジューリング時、ローカルプレフィックスキャッシュに加えて外部KVキャッシュも問い合わせる。
sequenceDiagram
participant S as Scheduler
participant KV as KVConnector (SCHEDULER)
participant CM as KVCacheManager
Note over S: _schedule_waiting()
S->>CM: get_computed_blocks(request)
CM-->>S: local_computed_tokens
S->>KV: get_num_new_matched_tokens(request, local_computed_tokens)
KV-->>S: (external_tokens, is_async)
Note over S: total = local + external
S->>CM: allocate_slots(request, num_new_tokens, ..., delay_cache_blocks=is_async)
CM-->>S: new_blocks
S->>KV: update_state_after_alloc(request, blocks, external_tokens)
alt is_async=True
Note over S: request.status = WAITING_FOR_REMOTE_KVS
else is_async=False
Note over S: request.status = RUNNING
end
Note over S: schedule()末尾
S->>KV: build_connector_meta(scheduler_output)
KV-->>S: KVConnectorMetadata
参照: target/vllm/vllm/v1/core/sched/scheduler.py:608-772
WAITING_FOR_REMOTE_KVS 状態管理
非同期KVロード中のリクエストはWAITING_FOR_REMOTE_KVS状態に遷移する。
_update_from_kv_xfer_finished(): Worker側コネクタのfinished_recving/finished_sendingを処理finished_recving→finished_recving_kv_req_idsに追加(次stepで処理)finished_sending→ ブロック即時解放
_update_waiting_for_remote_kv(): WAITING_FOR_REMOTE_KVS状態のリクエストを確認- 受信完了 →
kv_cache_manager.cache_blocks()でキャッシュ → WAITING状態に戻す - ロードエラー → 有効なcomputed_tokensだけキャッシュ、またはブロック解放
- 受信完了 →
参照: target/vllm/vllm/v1/core/sched/scheduler.py:1961-2034
リクエスト完了時の遅延解放
_connector_finished()はrequest_finished()を呼び、コネクタがブロックの非同期送信を引き受けるかを確認する。
delay_free, kv_xfer_params = connector.request_finished(request, block_ids)
# delay_free=True → ブロックはget_finished()で送信完了報告後に解放
# kv_xfer_params → リクエスト出力に含めるKV転送パラメータ
参照: target/vllm/vllm/v1/core/sched/scheduler.py:1930-1959
Worker/GPUModelRunner 統合
KVConnectorModelRunnerMixin
GPUModelRunnerにミックスインされ、KVコネクタのライフサイクルを管理する。
_get_kv_connector_output() — コアライフサイクル
@contextmanager
def _get_kv_connector_output(scheduler_output, wait_for_save=True):
output = KVConnectorOutput()
kv_connector = get_kv_transfer_group()
# 1. Scheduler側メタデータをバインド
kv_connector.bind_connector_metadata(scheduler_output.kv_connector_metadata)
# 2. 非同期KVロード開始(forward前)
kv_connector.start_load_kv(get_forward_context())
try:
yield output # ← ここでモデルforward実行(save_kv_layer含む)
finally:
# 3. セーブ完了待機
if wait_for_save:
kv_connector.wait_for_save()
# 4. 完了・エラー情報収集
output.finished_sending, output.finished_recving = (
kv_connector.get_finished(scheduler_output.finished_req_ids))
output.invalid_block_ids = kv_connector.get_block_ids_with_load_errors()
output.kv_connector_stats = kv_connector.get_kv_connector_stats()
output.kv_cache_events = kv_connector.get_kv_connector_kv_cache_events()
# 5. メタデータクリア
kv_connector.clear_connector_metadata()
参照: target/vllm/vllm/v1/worker/kv_connector_model_runner_mixin.py:80-111
execute_model() 内の呼び出し位置
# GPUModelRunner.execute_model()
# (1) プリエンプション処理
if scheduler_output.preempted_req_ids and has_kv_transfer_group():
get_kv_transfer_group().handle_preemptions(scheduler_output.preempted_req_ids)
# (2) forward不要時のKV転送
if num_reqs == 0:
return self.kv_connector_no_forward(scheduler_output, self.vllm_config)
# (3) モデルforward + KVコネクタ
with self.maybe_get_kv_connector_output(scheduler_output) as kv_connector_output:
model_output = self._model_forward(...)
# (4) sample_tokens()に引き渡し
self.kv_connector_output = kv_connector_output # ephemeral state
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:3330-3617
KVコネクタ初期化
GPUModelRunner._initialize_kv_caches()内で、KVキャッシュ確保後にコネクタを登録する。
if has_kv_transfer_group():
kv_transfer_group = get_kv_transfer_group()
if self.cross_layers_kv_cache is not None:
kv_transfer_group.register_cross_layers_kv_cache(
self.cross_layers_kv_cache, self.cross_layers_attn_backend)
else:
kv_transfer_group.register_kv_caches(kv_caches)
kv_transfer_group.set_host_xfer_buffer_ops(copy_kv_blocks)
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:6079-6088
KVConnectorOutput
Worker→Schedulerへのフィードバック構造体。
| フィールド | 型 | 用途 |
|---|---|---|
finished_sending | set[str] | None | 送信完了リクエストID |
finished_recving | set[str] | None | 受信完了リクエストID |
kv_connector_stats | KVConnectorStats | None | 転送統計 |
kv_cache_events | KVConnectorKVEvents | None | ブロック保存/削除イベント |
invalid_block_ids | set[int] | ロード失敗ブロックID |
expected_finished_count | int | ハンドシェイクベースコネクタ用 |
参照: target/vllm/vllm/v1/outputs.py:123-148
KV Cache Events
外部システム(ルーティング、モニタリング等)へのKVキャッシュ状態通知システム。
イベント型
| イベント | フィールド | 発行タイミング |
|---|---|---|
BlockStored | block_hashes, parent_block_hash, token_ids, block_size, lora_id, medium | KVブロック保存時 |
BlockRemoved | block_hashes, medium | KVブロック削除時 |
AllBlocksCleared | なし | 全ブロッククリア時 |
参照: target/vllm/vllm/distributed/kv_events.py:49-84
イベントフロー
- Worker側コネクタが
BlockStored/BlockRemovedイベントを生成 _get_kv_connector_output()がイベントをKVConnectorOutput.kv_cache_eventsに収集- Scheduler側で
update_connector_output()→KVEventAggregatorで全Worker共通イベントを集約 take_events()で集約済みイベントを取得EventPublisher(ZMQ PUB/ROUTERまたはNull)で外部に配信
EventPublisher
| 実装 | 用途 |
|---|---|
NullEventPublisher | no-op(デフォルト) |
ZmqEventPublisher | ZMQ PUB/ROUTERで配信。インメモリreplay buffer付き |
参照: target/vllm/vllm/distributed/kv_events.py:205-473
KVTransferConfig
KV Transferの設定。--kv-transfer-configで指定する。
| フィールド | デフォルト | 用途 |
|---|---|---|
kv_connector | None | コネクタ名(“LMCacheConnectorV1“等) |
kv_role | None | "kv_producer" / "kv_consumer" / "kv_both" |
engine_id | UUID | エンジン識別子 |
kv_buffer_device | “cuda” | バッファデバイス |
kv_buffer_size | 1e9 | バッファサイズ(バイト) |
kv_rank | None | P/D内のランク(0=prefill, 1=decode) |
kv_parallel_size | 1 | 並列インスタンス数 |
kv_ip / kv_port | “127.0.0.1” / 14579 | 接続先 |
kv_connector_extra_config | {} | コネクタ固有追加設定 |
kv_connector_module_path | None | 動的ロード用モジュールパス |
kv_load_failure_policy | “recompute” | ロード失敗時ポリシー(“recompute” or “fail”) |
参照: target/vllm/vllm/config/kv_transfer.py:17-117
ECConnectorとの比較
| 観点 | KV Transfer | ECConnector |
|---|---|---|
| 対象 | デコーダKVキャッシュ | エンコーダキャッシュ |
| 基底クラス | KVConnectorBase_V1 | ECConnectorBase |
| abstractメソッド数 | 7 | 5 |
| ロール分離 | SCHEDULER / WORKER | SCHEDULER / WORKER |
| Factory | KVConnectorFactory | ECConnectorFactory |
| Mixin | KVConnectorModelRunnerMixin | ECConnectorModelRunnerMixin |
| レイヤー別操作 | あり(save/load per layer) | なし(エンコーダ出力一括) |
| 非同期ロード | あり(WAITING_FOR_REMOTE_KVS) | なし |
| イベント通知 | あり(BlockStored/Removed) | なし |
| 登録済み実装数 | 10 | 2(Example, SHM) |
| 設定クラス | KVTransferConfig | ECTransferConfig |
ディレクトリ構造
target/vllm/vllm/distributed/kv_transfer/
├── kv_connector/
│ ├── base.py # KVConnectorBase リダイレクト
│ ├── factory.py # KVConnectorFactory + 10コネクタ登録
│ ├── utils.py
│ └── v1/
│ ├── base.py # KVConnectorBase_V1 (抽象基底)
│ ├── metrics.py # KVConnectorStats, Prometheus
│ ├── example_connector.py # safetensorsデバッグ用
│ ├── lmcache_connector.py # LMCacheラッパー
│ ├── lmcache_mp_connector.py
│ ├── nixl_connector.py
│ ├── offloading_connector.py
│ ├── multi_connector.py # 複合コネクタ
│ ├── decode_bench_connector.py
│ ├── lmcache_integration/ # native LMCache実装
│ │ ├── vllm_v1_adapter.py
│ │ ├── multi_process_adapter.py
│ │ └── utils.py
│ ├── p2p/ # P2P NCCLコネクタ
│ ├── moriio/ # MORIIOコネクタ
│ └── mooncake/ # Mooncakeコネクタ
├── kv_transfer_state.py # グローバル状態管理
└── __init__.py
target/vllm/vllm/distributed/kv_events.py # イベントシステム
target/vllm/vllm/config/kv_transfer.py # KVTransferConfig
target/vllm/vllm/v1/worker/kv_connector_model_runner_mixin.py # Mixin
依存関係
上流(KV Transferを使う側)
| コンポーネント | 使い方 |
|---|---|
| Scheduler | 外部キャッシュ問い合わせ、メタデータ構築、完了処理 |
| GPUModelRunner | Mixinでライフサイクル管理、KVキャッシュ登録 |
| Attention層 | wait_for_layer_load() / save_kv_layer() 呼び出し |
下流(KV Transferが使う側)
| コンポーネント | 使い方 |
|---|---|
| KVCacheManager | ブロック割り当て・解放情報の取得 |
| ForwardContext | KVキャッシュテンソルへのアクセス |
| LMCache / NIXL / etc. | 実際のKVデータ保存・取得 |
マルチモーダル処理パイプライン サマリー
深度: [MEDIUM] 確信度: [VERIFIED] 最終更新: 2026-02-11
概要
vLLMのマルチモーダル処理パイプラインは、画像・音声・動画等の非テキストデータをLLMの推論に統合するシステムである。フロントエンド(P0)でのメディア前処理・キャッシュと、バックエンド(P1)でのエンコーダ実行・埋め込みマージの2段構成で動作する。
エンドツーエンド データフロー
graph TD
A["API Request<br>(messages + images)"] --> B["ChatTemplate 適用<br>プレースホルダー挿入"]
B --> C["InputPreprocessor<br>トークナイズ + HF Processor"]
C --> D{"ProcessorCache<br>ヒット?"}
D -->|HIT| E["キャッシュから取得<br>(HF処理スキップ)"]
D -->|MISS| F["HF Processor 実行<br>pixel_values テンソル生成"]
E --> G["MultiModalFeatureSpec 構築"]
F --> G
G --> H["EngineCoreRequest<br>ZMQ IPC 送信"]
H --> I["Scheduler"]
I --> J{"EncoderCacheManager<br>ヒット?"}
J -->|HIT| K["エンコーダ計算スキップ"]
J -->|MISS| L["encoder_compute_budget<br>から割り当て"]
K --> M["GPUModelRunner"]
L --> M
M --> N["_execute_mm_encoder()<br>model.embed_multimodal()"]
N --> O["encoder_cache に格納"]
O --> P["_gather_mm_embeddings()<br>キャッシュからスライス"]
P --> Q["embed_input_ids()<br>text + vision マージ<br>(masked_scatter_)"]
Q --> R["model.forward()<br>統合推論"]
主要コンポーネント
| コンポーネント | 場所 | 役割 |
|---|---|---|
MULTIMODAL_REGISTRY | vllm/multimodal/registry.py | モデルごとのプロセッサ/情報を登録・取得 |
BaseMultiModalProcessor | vllm/multimodal/processing/processor.py | HFプロセッサ実行、プロンプト更新管理 |
MultiModalHasher | vllm/multimodal/hasher.py | コンテンツベースハッシュ(blake3) |
ProcessorCache (4種) | vllm/multimodal/cache.py | P0側のHF処理結果キャッシュ |
EncoderCacheManager | vllm/v1/core/encoder_cache_manager.py | P1側のエンコーダ出力の論理管理 |
encoder_cache | vllm/v1/worker/gpu_model_runner.py:439 | GPU上のエンコーダ出力テンソルキャッシュ |
キャッシュの3層構造
P0(フロントエンド) P1(Scheduler) P1(GPU)
┌──────────────────┐ ┌─────────────────┐ ┌──────────────┐
│ ProcessorCache │ │ EncoderCache │ │ encoder_cache│
│ LRU, サイズベース │ │ Manager │ │ dict[str, │
│ │ │ RefCount + FIFO │ │ Tensor] │
│ 何をキャッシュ: │ │ │ │ │
│ HF処理済みテンソル │ │ 何を管理: │ │ 何をキャッシュ:│
│ + prompt_updates │ │ 容量・参照カウント│ │ エンコーダ出力│
│ │ │ Evictionリスト │ │ (GPUテンソル) │
└──────────────────┘ └─────────────────┘ └──────────────┘
ヒット時: ヒット時: ヒット時:
HF処理スキップ エンコーダ計算スキップ テンソル再利用
IPC転送量削減 予算節約 再計算不要
テキスト推論との主な差分
| 処理段階 | テキスト推論 | マルチモーダル推論 |
|---|---|---|
| 入力前処理 | tokenize のみ | tokenize + HF Processor + ハッシュ + キャッシュ |
| プロンプト | テキストトークンのみ | テキスト + プレースホルダートークン(<start_of_image> 等) |
| EngineCoreRequest | mm_features = None | mm_features = [MultiModalFeatureSpec, ...] |
| Scheduler | KVキャッシュ予算のみ | + エンコーダ計算予算管理 |
| GPUModelRunner | input_ids → model.forward() | encoder実行 → embed_input_ids(masked_scatter_) → inputs_embeds → model.forward() |
Gemma3 固有の特徴
- ビジョンエンコーダ: SiglipVisionModel(SIGLIP ViT、双方向Attention)
- プロジェクタ: AvgPool2d → GemmaRMSNorm → Linear(vision → text空間)
- プレースホルダー:
<start_of_image>→image_token × 256に展開 - Pan-and-Scan: アスペクト比が大きい画像を複数クロップ(V1では簡略化されたアテンション)
- 改行トークン結合:
\n+\n\n→\n\n\n等の特殊処理
詳細ドキュメント
| ドキュメント | 内容 |
|---|---|
| mm-processing.md | フロントエンド: チャットテンプレート、プレースホルダー、MMハッシュ[DEEP](hash_kwargs/serialize_item/iter_item_to_bytes詳細、identifier vs mm_hash使い分け、プレフィックスキャッシュ連携)、プロセッサキャッシュ4種、ZMQ送信データ |
| mm-engine-gpu.md | バックエンド: EncoderCacheManager、Schedulerエンコーダ予算、GPUModelRunnerエンコーダ実行・キャッシュ・埋め込みマージ |
| gemma3-vision.md | Gemma3: SiglipVisionModel、MultiModalProjector、Pan-and-Scan、masked_scatter_マージ |
上流・下流
- 上流: エントリポイント → InputProcessor
- 下流: Scheduler → GPUModelRunner → モデル層
主要ファイル
| ファイル | 概要 |
|---|---|
target/vllm/vllm/multimodal/ | マルチモーダル処理の基盤(レジストリ、ハッシュ、キャッシュ、パース) |
target/vllm/vllm/v1/engine/input_processor.py | フロントエンドでのMM処理統合 |
target/vllm/vllm/v1/core/encoder_cache_manager.py | バックエンドのエンコーダキャッシュ管理 |
target/vllm/vllm/v1/worker/gpu_model_runner.py | GPU上でのエンコーダ実行と埋め込みマージ |
target/vllm/vllm/model_executor/models/gemma3_mm.py | Gemma3のマルチモーダル実装 |
target/vllm/vllm/model_executor/models/siglip.py | SiglipVisionModel(ビジョンエンコーダ) |
Gemma3 ビジョンエンコーダと画像処理 [MEDIUM] [VERIFIED]
最終更新: 2026-02-11
Gemma3 モデルにおけるビジョンエンコーダ(SiglipVisionModel)、プロジェクタ(Gemma3MultiModalProjector)、および text + vision 埋め込みマージの詳細。
モデルアーキテクチャ全体像
graph TD
subgraph "Gemma3ForConditionalGeneration"
subgraph "vision_tower (SiglipVisionModel)"
PE["SiglipVisionEmbeddings<br>Conv2d patch embedding<br>+ position embedding"]
ENC["SiglipEncoder<br>N層 Transformer<br>(双方向Attention)"]
PE --> ENC
end
PROJ["Gemma3MultiModalProjector<br>AvgPool2d → RMSNorm → Linear"]
ENC --> PROJ
subgraph "language_model (Gemma3ForCausalLM)"
EMB["embed_tokens()<br>テキスト埋め込み"]
LM["Transformer Decoder<br>(因果的Attention)"]
EMB --> LM
end
MERGE["masked_scatter_<br>text + vision マージ"]
PROJ --> MERGE
EMB --> MERGE
MERGE --> LM
end
参照: target/vllm/vllm/model_executor/models/gemma3_mm.py:481-694
1. SiglipVisionModel(ビジョンエンコーダ)
構造
参照: target/vllm/vllm/model_executor/models/siglip.py:848-894
SiglipVisionModel
└─ SiglipVisionTransformer (L681)
├─ SiglipVisionEmbeddings (L282)
│ ├─ patch_embedding: Conv2d(3, hidden_size, kernel=patch_size, stride=patch_size)
│ └─ position_embedding: Embedding(num_patches, hidden_size)
├─ SiglipEncoder (L520)
│ └─ layers: ModuleList[SiglipEncoderLayer] × N
│ ├─ layer_norm1 → SiglipAttention (MMEncoderAttention)
│ └─ layer_norm2 → SiglipMLP
└─ post_layernorm: LayerNorm(最終層のみ)
パッチ埋め込み (SiglipVisionEmbeddings)
参照: target/vllm/vllm/model_executor/models/siglip.py:282-352
# 入力: pixel_values (batch, 3, H, W)
patch_embeds = self.patch_embedding(pixel_values) # Conv2d: (batch, hidden_size, grid, grid)
embeddings = patch_embeds.flatten(2).transpose(1, 2) # (batch, num_patches, hidden_size)
embeddings += self.position_embedding(position_ids) # 位置埋め込みを加算
image_size = 256の場合、patch_size = 16→num_patches = (256/16)² = 256- 各パッチは
16×16×3 = 768ピクセルからhidden_size次元のベクトルに変換 - 位置埋め込みは学習済みの
nn.Embedding(補間対応あり)
エンコーダ層 (SiglipEncoder)
参照: target/vllm/vllm/model_executor/models/siglip.py:520-567
各エンコーダ層の構造:
SiglipEncoderLayer:
residual = hidden_states
hidden_states = layer_norm1(hidden_states)
hidden_states = self_attn(hidden_states) ← MMEncoderAttention(双方向)
hidden_states = residual + hidden_states
residual = hidden_states
hidden_states = layer_norm2(hidden_states)
hidden_states = mlp(hidden_states)
hidden_states = residual + hidden_states
- アテンション型:
MMEncoderAttention(双方向、因果マスクなし) - 全パッチが全パッチを参照できる(テキストの因果アテンションとは異なる)
forward() フロー
参照: target/vllm/vllm/model_executor/models/siglip.py:755-788
def forward(self, pixel_values, *, select_layers=None, feature_select_strategy=None):
hidden_states = self.embeddings(pixel_values) # (batch, num_patches, hidden_size)
encoder_outputs = self.encoder(inputs_embeds=hidden_states) # N層 Transformer
encoder_outputs = resolve_visual_encoder_outputs( # 特徴選択
encoder_outputs, select_layers, feature_select_strategy
)
return self.last_hs_proc(encoder_outputs) # post_layernorm + head
出力: (batch, num_patches, hidden_size) — 典型的に (batch, 256, 1152)
2. Gemma3MultiModalProjector(投射層)
参照: target/vllm/vllm/model_executor/models/gemma3_mm.py:432-473
ビジョンエンコーダの出力をテキスト埋め込み空間に変換する。
パラメータ
mm_input_projection_weight: (vision_hidden_size, text_hidden_size) # 例: (1152, 2048)
mm_soft_emb_norm: GemmaRMSNorm(vision_hidden_size)
avg_pool: AvgPool2d(kernel_size, stride=kernel_size)
設定値の計算
patches_per_image = image_size // patch_size # 256 // 16 = 16
tokens_per_side = int(mm_tokens_per_image ** 0.5) # int(256 ** 0.5) = 16
kernel_size = patches_per_image // tokens_per_side # 16 // 16 = 1
kernel_size = 1 の場合、AvgPool2dは実質的にno-op(パッチ数が変わらない)。mm_tokens_per_image が小さい設定ではダウンサンプリングが発生する。
forward() フロー
def forward(self, vision_outputs):
# vision_outputs: (batch, hidden_size, num_patches) — エンコーダ出力のtranspose形式
batch_size, _, seq_length = vision_outputs.shape
# 1. 2Dグリッドへリシェイプ
reshaped = vision_outputs.transpose(1, 2).reshape(
batch_size, seq_length, patches_per_image, patches_per_image
) # (batch, seq_length, 16, 16)
# 2. 平均プーリング(ダウンサンプリング)
pooled = self.avg_pool(reshaped) # kernel=1の場合: (batch, seq_length, 16, 16)
pooled = pooled.flatten(2).transpose(1, 2) # (batch, mm_tokens_per_image, seq_length)
# 3. RMS正規化
normed = self.mm_soft_emb_norm(pooled)
# 4. 線形投射: vision_hidden_size → text_hidden_size
projected = torch.matmul(normed, self.mm_input_projection_weight)
# (batch, mm_tokens_per_image, text_hidden_size)
return projected.type_as(vision_outputs)
重要な注意: テキスト埋め込みの正規化とは異なり、ビジョン埋め込みには mm_soft_emb_norm のみが適用される。vocab embedding に適用されるスケーリング(embed_tokens * normalizer)はビジョン埋め込みには適用 されない。
3. embed_multimodal() と _process_image_input()
参照: target/vllm/vllm/model_executor/models/gemma3_mm.py:567-594
GPUModelRunner の _execute_mm_encoder() から呼ばれるメインのエンコーダ実行パス:
def embed_multimodal(self, **kwargs):
image_input = self._parse_and_validate_image_input(**kwargs) # pixel_values 取得
if image_input is None:
return []
return self._process_image_input(image_input)
def _process_image_input(self, image_input):
pixel_values = image_input["pixel_values"] # (total_patches, 3, image_size, image_size)
num_patches = image_input["num_patches"] # (num_images,) — 画像ごとのパッチ数
# ビジョンエンコーダ実行
image_features = self._image_pixels_to_features(self.vision_tower, pixel_values)
# プロジェクタで投射
image_embeds = self.multi_modal_projector(image_features)
# 画像ごとに分割 + flatten
return [e.flatten(0, 1) for e in image_embeds.split(num_patches.tolist())]
# list[Tensor(mm_tokens_per_image, text_hidden_size)]
テンソル形状の追跡
画像1枚、Pan-and-Scan なし(num_patches=1)の場合:
| ステージ | 形状 | 説明 |
|---|---|---|
| 入力 pixel_values | (1, 3, 256, 256) | RGB画像 |
| patch_embedding | (1, 1152, 16, 16) | Conv2d出力 |
| flatten + transpose | (1, 256, 1152) | パッチシーケンス |
| + position_embedding | (1, 256, 1152) | 位置情報付加 |
| SiglipEncoder (N層) | (1, 256, 1152) | Transformer処理 |
| Projector reshape | (1, 256, 16, 16) | 2Dグリッド |
| AvgPool2d (k=1) | (1, 256, 16, 16) | no-op |
| flatten + transpose | (1, 256, 256) | ? |
| mm_soft_emb_norm | (1, 256, 1152) | RMS正規化 |
| matmul(projection) | (1, 256, 2048) | テキスト空間 |
| flatten(0,1) | (256, 2048) | 最終出力 |
4. テキスト + ビジョン埋め込みのマージ
embed_input_ids()
参照: target/vllm/vllm/model_executor/models/gemma3_mm.py:596-614
def embed_input_ids(self, input_ids, multimodal_embeddings=None, *, is_multimodal=None, ...):
if multimodal_embeddings is None or is_multimodal is None:
return super().embed_input_ids(input_ids) # テキストのみ
return super().embed_input_ids(
input_ids, multimodal_embeddings=multimodal_embeddings,
is_multimodal=is_multimodal, handle_oov_mm_token=True,
)
_merge_multimodal_embeddings()
参照: target/vllm/vllm/model_executor/models/utils.py:445-487
def _merge_multimodal_embeddings(inputs_embeds, multimodal_embeddings, is_multimodal):
mm_embeds_flat = _flatten_embeddings(multimodal_embeddings)
# in-place 置換: is_multimodal=True の位置を mm_embeds_flat で上書き
inputs_embeds.masked_scatter_(
is_multimodal.unsqueeze(-1),
mm_embeds_flat.to(dtype=inputs_embeds.dtype)
)
return inputs_embeds
masked_scatter_ の動作:
is_multimodal:(seq_len,)のboolテンソル(True = 画像プレースホルダー位置)is_multimodal.unsqueeze(-1):(seq_len, 1)→ ブロードキャストで(seq_len, hidden_size)に展開mm_embeds_flat: True位置の数 × hidden_size の連続テンソル- True位置に順番に mm_embeds_flat の値を書き込む
制約: is_multimodal.sum() == len(mm_embeds_flat) でなければランタイムエラー
handle_oov_mm_token
Gemma3 は handle_oov_mm_token=True を指定。これは image_token_id が vocab の範囲外の場合でも安全に処理するための仕組み。
5. Pan-and-Scan(パノラマクロップ)
参照: target/vllm/vllm/model_executor/models/gemma3_mm.py:109-176
アスペクト比が大きい画像に対して、複数のクロップを生成して詳細認識を向上させる仕組み。
クロップ数の計算
def get_num_crops(self, *, image_width, image_height, processor):
# 横長画像の場合
if image_width >= image_height:
if width/height < min_ratio: return 0 # 比率が小さすぎる
num_crops_w = min(floor(width/min_crop_size), floor(w/h + 0.5))
num_crops_w = max(2, num_crops_w)
num_crops_w = min(max_num_crops, num_crops_w)
num_crops_h = 1
# 縦長画像の場合は逆
...
# クロップサイズが小さすぎる場合は無効
if min(crop_size_w, crop_size_h) < min_crop_size:
return 0
return num_crops_w * num_crops_h
Pan-and-Scan時のプロンプト
"Here is the original image <full_image_seq> and here are some crops to help you see better <full_image_seq> <full_image_seq>"
各 <full_image_seq> は image_seq_length(=256)トークンを消費。
V1での制限: Pan-and-Scan は簡略化されたアテンションパターンを使用するため、最適ではない結果になる可能性がある。
6. forward() — 最終推論
参照: target/vllm/vllm/model_executor/models/gemma3_mm.py:616-635
def forward(self, input_ids, positions, intermediate_tensors=None, inputs_embeds=None, **kwargs):
if intermediate_tensors is not None:
inputs_embeds = None # Pipeline Parallelism の中間ランク
hidden_states = self.language_model.model(
input_ids,
positions,
intermediate_tensors,
inputs_embeds=inputs_embeds, # text + vision マージ済み
**kwargs,
)
return hidden_states
マルチモーダル時は inputs_embeds が渡され、input_ids は使用されない(embed_input_ids で既に埋め込み済みのため)。
データフロー全体
pixel_values: (total_patches, 3, 256, 256)
│
▼
SiglipVisionEmbeddings
Conv2d(3→1152, k=16, s=16) + position_embedding
│
▼
(total_patches, 256, 1152)
│
▼
SiglipEncoder (N層 Transformer, 双方向Attention)
│
▼
(total_patches, 256, 1152)
│
▼
Gemma3MultiModalProjector
reshape → AvgPool2d → RMSNorm → matmul(1152→2048)
│
▼
(total_patches, 256, 2048)
│
▼
split by num_patches → list[(mm_tokens, 2048)]
│
▼
encoder_cache[mm_hash] に格納
│
▼
_gather_mm_embeddings() でスライス
│
▼
embed_input_ids():
text_embeds = embed_tokens(input_ids) # (seq_len, 2048)
merged = masked_scatter_(text_embeds, is_multimodal, mm_embeds)
│
▼
language_model.model(inputs_embeds=merged) # Gemma3 Decoder
主要ファイル
| ファイル | 主要クラス/関数 | 行 |
|---|---|---|
target/vllm/vllm/model_executor/models/gemma3_mm.py | Gemma3ForConditionalGeneration | L481 |
target/vllm/vllm/model_executor/models/gemma3_mm.py | Gemma3MultiModalProjector | L432 |
target/vllm/vllm/model_executor/models/gemma3_mm.py | Gemma3ProcessingInfo, Gemma3MultiModalProcessor | L77, L276 |
target/vllm/vllm/model_executor/models/siglip.py | SiglipVisionModel | L848 |
target/vllm/vllm/model_executor/models/siglip.py | SiglipVisionEmbeddings | L282 |
target/vllm/vllm/model_executor/models/siglip.py | SiglipEncoder | L520 |
target/vllm/vllm/model_executor/models/utils.py | _merge_multimodal_embeddings() | L445 |
関連ドキュメント
バックエンド マルチモーダル処理パス [MEDIUM] [VERIFIED]
最終更新: 2026-02-11
EngineCore(P1)でマルチモーダルリクエストがどのように処理されるかを追跡する。EncoderCacheManager、Schedulerのエンコーダ予算管理、GPUModelRunnerのエンコーダ実行・キャッシュ・埋め込みマージを含む。
全体フロー
sequenceDiagram
participant S as Scheduler
participant ECM as EncoderCacheManager
participant MR as GPUModelRunner
participant M as Model
Note over S: schedule() 実行中
S->>ECM: check_and_update_cache(req, i)
alt キャッシュヒット
ECM-->>S: True(エンコード不要)
else キャッシュミス
ECM-->>S: False
S->>ECM: can_allocate(req, i, budget, scheduled)
alt 空き/回収可能
ECM-->>S: True(Eviction実行の可能性あり)
S->>ECM: allocate(req, i)
Note over S: scheduled_encoder_inputs[req_id].append(i)
else 不足
ECM-->>S: False
Note over S: num_new_tokens を調整
end
end
Note over S: SchedulerOutput 構築
S->>MR: execute_model(scheduler_output)
MR->>MR: _batch_mm_inputs_from_scheduler()
MR->>M: embed_multimodal(pixel_values)
M-->>MR: encoder_outputs
MR->>MR: encoder_cache[mm_hash] = output
MR->>MR: _gather_mm_embeddings()
Note over MR: encoder_cache からスライス
MR->>M: embed_input_ids(ids, mm_embeds, is_multimodal)
Note over M: masked_scatter_ で text + vision マージ
MR->>MR: free_encoder_mm_hashes の処理
Note over MR: encoder_cache.pop(mm_hash)
1. EncoderCacheManager
概要
参照: target/vllm/vllm/v1/core/encoder_cache_manager.py:17-267
ビジョンエンコーダの出力(埋め込みテンソル)のライフサイクルを管理する。リファレンスカウント方式でリクエスト間のキャッシュ共有を実現し、遅延Evictionでメモリ効率を高める。
データ構造
| フィールド | 型 | 説明 |
|---|---|---|
cache_size | int | キャッシュ容量(エンコーダ埋め込み数単位) |
num_free_slots | int | 現在の空き容量 |
num_freeable_slots | int | 回収可能な容量(参照なしエントリ含む) |
cached | dict[str, set[str]] | mm_hash → 参照中のrequest_id集合 |
freeable | OrderedDict[str, int] | mm_hash → 埋め込み数(参照なし、回収可能) |
freed | list[str] | 実際にEvictされたmm_hashのリスト |
主要操作
check_and_update_cache(request, input_id) → bool
参照: target/vllm/vllm/v1/core/encoder_cache_manager.py:91-117
1. mm_hash が cached にない → False(キャッシュミス)
2. cached[mm_hash] が空集合(参照なし)→ freeable から除去、num_freeable_slots 減算
3. cached[mm_hash] に request_id 追加 → True(キャッシュヒット)
キャッシュヒット時、エンコーダ計算が 完全にスキップ される。
can_allocate(request, input_id, budget, scheduled) → bool
参照: target/vllm/vllm/v1/core/encoder_cache_manager.py:119-178
1. num_embeds > encoder_compute_budget → False(予算不足)
2. total ≤ num_free_slots → True(空きあり)
3. total > num_freeable_slots → False(回収しても不足)
4. total > num_free_slots かつ ≤ num_freeable_slots
→ Eviction 実行: freeable から oldest-first で popitem(last=False)
→ cached から削除、freed に追加
→ num_free_slots 回復 → True
Eviction ポリシー: FIFO順(OrderedDict の挿入順)。最も古い unreferenced エントリを先にEvict。
allocate(request, input_id)
参照: target/vllm/vllm/v1/core/encoder_cache_manager.py:180-205
キャッシュスペースを予約(論理的な簿記のみ)。物理メモリの割り当てはGPUModelRunnerで行われる。
free(request)
参照: target/vllm/vllm/v1/core/encoder_cache_manager.py:242-253
リクエスト完了時に呼ばれる。全エンコーダ入力の参照を解放。参照セットが空になったエントリは freeable に移動する(物理メモリは解放しない)。
get_freed_mm_hashes() → list[str]
参照: target/vllm/vllm/v1/core/encoder_cache_manager.py:255-266
Evictされたmm_hashを返してクリア。SchedulerOutputに含められ、GPUModelRunnerに通知される。
キャッシュ予算の計算
参照: target/vllm/vllm/v1/core/encoder_cache_manager.py:269-316
encoder_compute_budget = max(max_num_encoder_input_tokens, max_tokens_per_mm_item)
encoder_cache_size = max(encoder_cache_size_config, max_tokens_per_mm_item)
encoder_compute_budget: 1ステップあたりのエンコーダ計算量上限(埋め込み数)encoder_cache_size: キャッシュ全体の容量
2. Scheduler のエンコーダスケジューリング
_get_encoder_budget()
参照: target/vllm/vllm/v1/core/sched/scheduler.py:1060-1215
各リクエストのエンコーダ入力をスケジューリングするロジック。mm_features の各アイテムについて:
for i, mm_feature in enumerate(mm_features):
1. 位置チェック: start_pos + num_encoder_tokens がスケジュール範囲内か
2. 重複チェック: 同じ mm_hash が既にスケジュール済みか
3. キャッシュチェック: encoder_cache_manager.check_and_update_cache() → True ならスキップ
4. チャンクMMチェック: disable_chunked_mm_input の場合、部分スケジュール禁止
5. 割り当てチェック: can_allocate() → False ならトークン数調整して break
6. ECConnectorチェック: 外部キャッシュにある場合は external_load_encoder_input に追加
7. encoder_inputs_to_schedule に追加、予算減算
SchedulerOutput のMM関連フィールド
参照: target/vllm/vllm/v1/core/sched/output.py:207-218
| フィールド | 型 | 説明 |
|---|---|---|
scheduled_encoder_inputs | dict[str, list[int]] | req_id → エンコーダ入力インデックスのリスト |
free_encoder_mm_hashes | list[str] | 解放すべきmm_hashのリスト |
スケジューリングの統合
schedule() のRUNNINGフェーズとWAITINGフェーズの両方で _get_encoder_budget() が呼ばれる:
schedule()
├─ encoder_compute_budget = max_num_encoder_input_tokens # 初期予算
│
├─ RUNNING リクエスト処理
│ └─ _get_encoder_budget(request, ...)
│ → encoder_inputs_to_schedule, 調整後の num_new_tokens
│ → allocate() 実行
│
├─ WAITING リクエスト処理
│ └─ _get_encoder_budget(request, ...)
│ → 同上
│
└─ SchedulerOutput 構築
├─ scheduled_encoder_inputs = {req_id: [input_ids], ...}
└─ free_encoder_mm_hashes = encoder_cache_manager.get_freed_mm_hashes()
3. GPUModelRunner のエンコーダ実行
encoder_cache
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:439
self.encoder_cache: dict[str, torch.Tensor] = {}
mm_hash をキーとして、エンコーダ出力テンソル(GPU上)を保持する単純なdictキャッシュ。
_batch_mm_inputs_from_scheduler()
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:2250-2291
SchedulerOutput の scheduled_encoder_inputs から、エンコーダに渡すデータをバッチにまとめる。
入力: scheduled_encoder_inputs = {req_id: [input_ids], ...}
出力: (mm_hashes, mm_kwargs, mm_lora_refs)
for req_id, encoder_input_ids in scheduled_encoder_inputs.items():
for mm_input_id in encoder_input_ids:
mm_feature = req_state.mm_features[mm_input_id]
if mm_feature.data is None: # P0キャッシュヒットで省略された場合
continue
mm_hashes.append(mm_feature.identifier)
mm_kwargs.append((modality, mm_feature.data))
_execute_mm_encoder()
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:2293-2447
1. mm_kwargs をモダリティごとにグループ化(group_mm_kwargs_by_modality)
2. 各グループに対して model.embed_multimodal(**mm_kwargs_group) を実行
3. LoRA tower mapping が必要な場合は事前にセット
4. 出力を encoder_cache[mm_hash] に格納
5. ECConnector があれば maybe_save_ec_to_connector() で外部キャッシュにも保存
バッチ処理: 同一モダリティのアイテムはバッチ実行される。異なるモダリティは別グループとして処理。
_gather_mm_embeddings()
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:2449-2556
スケジュールされたトークン範囲に対応するエンコーダ出力をキャッシュから取得し、マージ用のデータを準備する。
for req_id in input_batch.req_ids:
for mm_feature in req_state.mm_features:
1. プレースホルダーの範囲 [start_pos, start_pos + num_encoder_tokens)
2. スケジュールされた範囲 [num_computed_tokens, num_computed + num_scheduled)
3. 重複部分を計算 → start_idx, end_idx
4. encoder_cache[mm_hash] からスライス → mm_embeds_item
5. is_mm_embed マスクの対応位置を True に設定
チャンクPrefill対応: num_computed_tokens と num_scheduled_tokens に基づいて部分的な埋め込み取得が可能。
出力:
mm_embeds: list[torch.Tensor]— 全リクエストの埋め込みスライスを連結is_mm_embed: torch.Tensor—(total_num_scheduled_tokens,)のboolマスク
統合: _model_forward() 内の処理
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:2756-2777
# 1. エンコーダ実行 + キャッシュ格納
self._execute_mm_encoder(scheduler_output)
# 2. キャッシュから必要な埋め込みを収集
mm_embeds, is_mm_embed = self._gather_mm_embeddings(scheduler_output)
# 3. テキスト + ビジョン埋め込みのマージ
inputs_embeds_scheduled = self.model.embed_input_ids(
self.input_ids.gpu[:num_scheduled_tokens],
multimodal_embeddings=mm_embeds,
is_multimodal=is_mm_embed,
)
embed_input_ids() は内部で masked_scatter_ を使い、is_multimodal=True の位置を mm_embeds で置換する。
エンコーダキャッシュの解放
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:898-899
for mm_hash in scheduler_output.free_encoder_mm_hashes:
self.encoder_cache.pop(mm_hash, None)
SchedulerOutput に含まれる free_encoder_mm_hashes に基づいて、GPUメモリ上のテンソルを解放。
4. テキスト推論との差分まとめ
| 処理ステップ | テキスト推論 | マルチモーダル推論 |
|---|---|---|
| Scheduler: _get_encoder_budget | 不要(has_encoder_inputs=False) | エンコーダ入力のスケジューリング |
| Scheduler: encoder_compute_budget | 消費しない | ステップごとに予算管理 |
| SchedulerOutput | scheduled_encoder_inputs={} | {req_id: [input_ids]} |
| GPUModelRunner: encoder実行 | なし | _execute_mm_encoder() |
| GPUModelRunner: 埋め込み | input_ids をそのまま使用 | embed_input_ids() で text + vision マージ |
| GPUModelRunner: キャッシュ | なし | encoder_cache dict |
| モデルforward入力 | input_ids | inputs_embeds(テンソル) |
5. キャッシュの3層構造
P0(フロントエンド) P1(バックエンド/Scheduler) P1(バックエンド/GPU)
┌────────────────────┐ ┌───────────────────────┐ ┌──────────────────┐
│ ProcessorCache │ │ EncoderCacheManager │ │ encoder_cache │
│ (LRU, mm_hash) │ │ (RefCount, mm_hash) │ │ (dict, mm_hash) │
│ │ │ │ │ │
│ キャッシュ対象: │ │ キャッシュ対象: │ │ キャッシュ対象: │
│ HF処理済みテンソル │ │ 論理的な存在管理 │ │ GPU上のテンソル │
│ + prompt_updates │ │ (容量・参照カウント) │ │ (エンコーダ出力) │
│ │ │ │ │ │
│ Eviction: LRU │ │ Eviction: FIFO │ │ Eviction: │
│ (サイズベース) │ │ (OrderedDict oldest) │ │ Scheduler指示 │
└────────────────────┘ └───────────────────────┘ └──────────────────┘
↓ ↓ ↓
HF処理スキップ エンコーダ計算スキップ テンソル再利用
主要ファイル
| ファイル | 主要クラス/関数 | 行 |
|---|---|---|
target/vllm/vllm/v1/core/encoder_cache_manager.py | EncoderCacheManager | L17 |
target/vllm/vllm/v1/core/sched/scheduler.py | _get_encoder_budget() | L1060 |
target/vllm/vllm/v1/core/sched/output.py | SchedulerOutput (MM fields) | L207, L218 |
target/vllm/vllm/v1/worker/gpu_model_runner.py | _execute_mm_encoder(), _gather_mm_embeddings() | L2293, L2449 |
関連ドキュメント
フロントエンド マルチモーダル処理パス [MEDIUM→DEEP(§3)] [VERIFIED]
最終更新: 2026-02-17
APIリクエストに含まれる画像データが、フロントエンドプロセス(P0)でどのように処理され、EngineCoreRequest として ZMQ 経由でバックエンド(P1)へ送信されるかを追跡する。テキスト推論パスとの差分を中心に記述する。
全体フロー
graph TD
A["API Request<br>(messages + images)"] --> B["ChatTemplate適用"]
B --> C["InputPreprocessor.preprocess()"]
C --> D["_process_multimodal()"]
D --> E["mm_processor.info.parse_mm_data()"]
E --> F["mm_processor.apply()"]
F --> G{"ProcessorCache<br>ヒット?"}
G -->|HIT| H["キャッシュからitem+prompt_updates取得<br>(HF処理スキップ)"]
G -->|MISS| I["HF Processor実行<br>pixel_values等テンソル生成"]
I --> J["キャッシュに格納"]
H --> K["MultiModalInputs 構築"]
J --> K
K --> L["InputProcessor.process_inputs()"]
L --> M["MultiModalFeatureSpec 構築"]
M --> N["EngineCoreRequest<br>(mm_features)"]
N --> O["ZMQ IPC 送信"]
1. チャットテンプレートとプレースホルダー
テンプレート適用前後の文字列(Gemma3の例)
適用前(OpenAI形式のメッセージ):
{
"messages": [
{"role": "user", "content": [
{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}},
{"type": "text", "text": "この画像は何ですか?"}
]}
]
}
チャットテンプレート適用後(テキスト):
<bos><start_of_turn>user
<start_of_image>この画像は何ですか?<end_of_turn>
<start_of_turn>model
ここで <start_of_image> がプレースホルダートークンとなる。
プレースホルダーの展開
Gemma3ProcessingInfo.get_image_repl() がプレースホルダーを展開する。
参照: target/vllm/vllm/model_executor/models/gemma3_mm.py:178
<start_of_image> → processor.full_image_sequence
processor.full_image_sequence はHuggingFace Gemma3Processorが定義する完全なトークン列で、<start_of_image> + image_token × image_seq_length + <end_of_image> の形式。
Pan-and-Scan有効時(複数クロップ):
"Here is the original image <full_image_seq> and here are some crops to help you see better <full_image_seq> <full_image_seq>"
参照: target/vllm/vllm/model_executor/models/gemma3_mm.py:196-211
画像1枚あたりのトークン数
num_tokens = (num_crops + 1) * image_seq_length
参照: target/vllm/vllm/model_executor/models/gemma3_mm.py:213-230
image_seq_length: Gemma3Processorの設定値(典型的に256)num_crops: Pan-and-Scan無効時は0、有効時はアスペクト比に基づいて計算(最大pan_and_scan_max_num_crops)- よって画像1枚で256〜1280+トークンを消費
トークン列の構造
テキスト推論ではトークン列は純粋なテキストトークンのみ。マルチモーダルでは以下の構造になる:
[BOS] [start_of_turn] [user] [\n]
[start_of_image] [image_token × 256] [end_of_image] ← 画像プレースホルダー
[テキストトークン列...] ← "この画像は何ですか?"
[end_of_turn] [\n] [start_of_turn] [model] [\n]
image_token の位置がマスクで追跡され(PlaceholderRange)、後にビジョンエンコーダの出力で置換される。
改行トークンの結合処理
Gemma3固有の問題:\n\n\n と \n\n\n\n が単一トークンとして存在する。画像置換テキストに \n\n が挿入されると、隣接する \n と結合が必要。
参照: target/vllm/vllm/model_executor/models/gemma3_mm.py:351-385
# _apply_token_matches() でトークンの結合を実行
replace_token_matches(token_ids, [newline_1, newline_2], [newline_3]) # \n + \n\n → \n\n\n
replace_token_matches(token_ids, [newline_2, newline_1], [newline_3]) # \n\n + \n → \n\n\n
replace_token_matches(token_ids, [newline_2, newline_2], [newline_4]) # \n\n + \n\n → \n\n\n\n
2. マルチモーダルデータの処理パイプライン
InputPreprocessor._process_multimodal()
参照: target/vllm/vllm/inputs/preprocess.py:193-232
_process_multimodal(prompt, mm_data, mm_processor_kwargs, mm_uuids)
1. mm_processor.info.parse_mm_data(mm_data)
→ MultiModalDataItems(モダリティごとに型付きデータアイテムに変換)
2. mm_processor.apply(prompt, mm_items, ...)
→ MultiModalInputs(トークン列 + テンソルデータ + プレースホルダー位置 + ハッシュ)
BaseMultiModalProcessor.apply()
HFプロセッサ実行、プロンプト更新(プレースホルダー検出・展開)、キャッシュ管理を統合的に処理する。
主な出力(MultiModalInputs):
prompt_token_ids: プレースホルダー展開済みのトークン列mm_kwargs:dict[modality, list[MultiModalKwargsItem]]— 処理済みテンソルデータmm_hashes:dict[modality, list[str]]— 各アイテムのハッシュ値mm_placeholders:dict[modality, list[PlaceholderRange]]— プレースホルダー位置情報
Gemma3MultiModalProcessor._call_hf_processor()
参照: target/vllm/vllm/model_executor/models/gemma3_mm.py:277-310
親クラスのHFプロセッサ呼び出し後、num_patches を追加計算する。HFプロセッサはこの値をpopしてしまうため、vLLM側で再計算が必要。
num_crops = [self.info.get_num_crops(...) for size in image_sizes]
processed_outputs["num_patches"] = torch.tensor(num_crops) + 1 # +1 for original
HFプロセッサの出力:
pixel_values:(total_patches, 3, image_size, image_size)— 全パッチのピクセルテンソルnum_patches:(num_images,)— 画像ごとのパッチ数(= num_crops + 1)
3. MMハッシュ [DEEP] [VERIFIED]
マルチモーダル入力の同一性を判定するためのコンテンツベースハッシュ。2つの用途がある:
- ProcessorCache — HFプロセッサの処理結果キャッシュ(同じ画像の再処理回避)
- プレフィックスキャッシュのextra_keys — KVキャッシュブロックハッシュ計算時にブロック内のMMトークンを識別
3.1 ハッシュ計算の全体フロー
graph TD
A["BaseMultiModalProcessor<br>apply() / _cached_apply_hf_processor()"] --> B["_hash_mm_items()"]
B --> C{"mm_uuids<br>提供?"}
C -->|"あり + kwargs空"| D["item_uuid をそのまま使用"]
C -->|"なし or kwargs非空"| E["MultiModalHasher.hash_kwargs()"]
E --> F["kwargs をキー名でソート"]
F --> G["iter_item_to_bytes() で再帰的バイト変換"]
G --> H["serialize_item() で型別シリアライズ"]
H --> I["hasher.update() に逐次投入"]
I --> J["hasher.hexdigest() → mm_hash"]
3.2 _hash_mm_items() — ハッシュ入力の構成
参照: target/vllm/vllm/multimodal/processing/processor.py:1300-1364
各モダリティ(画像の場合 "image")ごとに、アイテム1つずつハッシュを計算する。
MultiModalHasher.hash_kwargs(
model_id=model_id, # e.g. "google/gemma-3-27b-it"
image=item, # PIL.Image.Image or MediaWithBytes
**hf_processor_mm_kwargs, # HFプロセッサに渡すkwargs(PaS設定等)
**tokenization_kwargs, # トークナイザkwargs
)
重要: ハッシュ入力は model_id + モダリティ名付きの画像データ + プロセッサkwargs + トークナイザkwargs。同じ画像でもプロセッサkwargsが異なればハッシュが変わる。
mm_uuids による分岐
_hash_mm_items() はアイテムごとに3つのパスを持つ:
| 条件 | 動作 |
|---|---|
item_uuid が None | 画像データから hash_kwargs() を計算 |
item_uuid あり + hf_processor_mm_kwargs or tokenization_kwargs が非空 | item_uuid 文字列を item として hash_kwargs() に投入(画像デコード回避) |
item_uuid あり + kwargs 空 | item_uuid をそのまま mm_hash として使用(ハッシュ計算スキップ) |
2番目のパスは最適化: UUID文字列のハッシュは画像ピクセルのハッシュより高速だが、kwargsとの組み合わせで一意性を保つ必要がある。
get_item_for_hash() — ハッシュ用アイテムの取得
参照: target/vllm/vllm/multimodal/parse.py:118-120
ProcessorBatchItems.get_item_for_hash() は get() と異なり、MediaWithBytes ラッパーを剥がさずにそのまま返す。
# get() → self._unwrap(self.data[index]) → PIL.Image(ラッパー除去)
# get_item_for_hash() → self.data[index] → MediaWithBytes[PIL.Image](ラッパー保持)
これにより serialize_item() で元のバイト列(JPEG/PNG等)からハッシュを計算でき、PIL画像のピクセルデコードを回避できる。
3.3 MultiModalHasher.hash_kwargs() — ハッシュ関数本体
参照: target/vllm/vllm/multimodal/hasher.py:154-162
@classmethod
def hash_kwargs(cls, **kwargs: object) -> str:
hasher = _get_hasher_factory(envs.VLLM_MM_HASHER_ALGORITHM)()
for k, v in sorted(kwargs.items(), key=lambda kv: kv[0]): # キー名でソート
for bytes_ in cls.iter_item_to_bytes(k, v):
hasher.update(bytes_)
return hasher.hexdigest()
ハッシュアルゴリズム: VLLM_MM_HASHER_ALGORITHM 環境変数で設定(target/vllm/vllm/envs.py:73,793)
blake3(デフォルト): 高速sha256/sha512: FIPS準拠用(政府・企業デプロイメント向け)
3.4 iter_item_to_bytes() — 再帰的バイト変換
参照: target/vllm/vllm/multimodal/hasher.py:134-151
dict/list/tupleを再帰的に展開し、キー名のバイト列 + 値のバイト列 を交互にyieldする。
# 例: image=PIL.Image(mode="RGB", data=...) の場合
# iter_item_to_bytes("image", {"mode": "RGB", "data": <numpy>})
# → "image.mode".encode() + "RGB".encode()
# → "image.data".encode() + <numpy raw bytes>
キー名がプレフィックスとして含まれるため、異なるkwargsキーの値が衝突しない(例: model_id の値と image の一部が同じバイト列でも区別される)。
3.5 serialize_item() — 型別シリアライズ
参照: target/vllm/vllm/multimodal/hasher.py:52-131
| データ型 | シリアライズ方法 | 備考 |
|---|---|---|
bytes / memoryview | そのまま返す | |
str | UTF-8エンコード | model_id 等 |
int / float | np.array(obj).tobytes() | |
PIL.Image (EXIF UUID) | exif[ImageID].bytes (16バイト) | 高速パス |
PIL.Image (通常) | {"mode": mode, "data": np.asarray(obj)} + palette | 全ピクセル読み込み |
MediaWithBytes(Image) (EXIF UUID) | 同上の高速パス | |
MediaWithBytes(Image) (通常) | original_bytes | エンコード済みバイト列。デコード不要で高速 |
torch.Tensor | numpy変換。bfloat16は view(uint8) 経由 | {"original_dtype", "original_shape", "data"} |
np.ndarray | {"dtype": str, "shape": tuple, "data": raw} | C-contiguous なら zero-copy |
| その他 | pickle.dumps() (警告ログ) | フォールバック |
画像の3つのシリアライズパス
graph TD
A["serialize_item(obj)"] --> B{"EXIF ImageID<br>UUID?"}
B -->|Yes| C["UUID.bytes (16バイト)<br>最速"]
B -->|No| D{"MediaWithBytes?"}
D -->|Yes| E["original_bytes<br>(JPEG/PNG等エンコード済み)<br>高速"]
D -->|No| F["mode + np.asarray(obj)<br>(全ピクセル展開)<br>低速"]
3.6 UUIDオーバーライド
キャッシュとプレフィックスキャッシュの両方が無効な場合、コンテンツハッシュの代わりに {request_id}-{modality}-{index} 形式のUUIDを使用する(ハッシュ計算コストの回避)。
参照: target/vllm/vllm/v1/engine/input_processor.py:551-574
3.7 LoRA対応のidentifier
LoRAのtower_connector_loraが有効な場合、同じ画像でもLoRAによって埋め込みが変わるため、identifier に LoRA名をプレフィックスとして付加する。
参照: target/vllm/vllm/v1/engine/input_processor.py:273-289
def _get_mm_identifier(self, mm_hash, lora_request):
if lora_request is None or not enable_tower_connector_lora:
return mm_hash
return f"{lora_request.lora_name}:{mm_hash}"
3.8 mm_hash vs identifier — 2つのハッシュ値の使い分け
| 属性 | mm_hash | identifier |
|---|---|---|
| 定義場所 | MultiModalFeatureSpec.mm_hash | MultiModalFeatureSpec.identifier |
| LoRAプレフィックス | なし | あり(enable_tower_connector_lora時) |
| 主な用途 | ProcessorCache のキー | EncoderCache / プレフィックスキャッシュのキー |
| 理由 | ProcessorCacheはLoRA非依存(ピクセルデータのみ) | エンコーダ出力・KVキャッシュはLoRAに依存 |
3.9 プレフィックスキャッシュでの使用
参照: target/vllm/vllm/v1/core/kv_cache_utils.py:388-444
ブロックハッシュ計算時に、ブロックのトークン範囲にMMプレースホルダが含まれる場合、mm_feature.identifier がextra_keysとして追加される。
def _gen_mm_extra_hash_keys(request, start_token_idx, end_token_idx, start_mm_idx):
# ブロック範囲とMMプレースホルダ範囲の重なりをチェック
while curr_mm_idx < len(mm_features):
mm_feature = mm_features[curr_mm_idx]
offset = mm_feature.mm_position.offset
length = mm_feature.mm_position.length
if end_token_idx > offset:
extra_keys.append(mm_feature.identifier) # ★ identifierを追加
...
これにより、同じトークン列でも異なる画像が挿入されたブロックは別のハッシュを持つ。逆に、同じ画像(同じidentifier)であればKVキャッシュのプレフィックスヒットが可能。
3.10 Gemma3における具体例
Gemma3は _hash_mm_items() をオーバーライドしていない。BaseMultiModalProcessor のデフォルト実装がそのまま使われる。
ハッシュ入力例:
model_id = "google/gemma-3-27b-it"
image = <MediaWithBytes[PIL.Image]> (元のJPEGバイト列)
※ hf_processor_mm_kwargs, tokenization_kwargs は通常空
→ hash_kwargs() 内部:
sorted keys: ["image", "model_id"]
1. "image" → iter_item_to_bytes("image", MediaWithBytes)
→ serialize_item() → original_bytes (JPEG/PNGバイト列)
2. "model_id" → iter_item_to_bytes("model_id", "google/gemma-3-27b-it")
→ "model_id".encode() + "google/gemma-3-27b-it".encode()
→ blake3.hexdigest() → "a1b2c3..." (mm_hash)
Pan-and-Scan(PaS)との関係: PaSによる画像分割はHFプロセッサ内で行われるため、ハッシュ計算の後に実行される。同じ画像 + 同じmodel_id + 同じkwargs → 同じmm_hash(PaS分割後の結果もキャッシュされる)。
4. プロセッサキャッシュ(P0側)
キャッシュタイプの選択
参照: target/vllm/vllm/multimodal/registry.py:284-320
mm_processor_cache_gb <= 0 → None(キャッシュ無効)
IPC非対応 or API process > 1 → processor_only
mm_processor_cache_type == "lru" → lru(Sender + Receiver)
mm_processor_cache_type == "shm" → shm(共有メモリ)
4種類のキャッシュ実装
参照: target/vllm/vllm/multimodal/cache.py
| 実装 | 場所 | 格納内容 | キャッシュヒット時の動作 |
|---|---|---|---|
MultiModalProcessorOnlyCache (L326) | P0のみ | テンソルデータ + prompt_updates | キャッシュから item + prompt_updates を返す(HF処理スキップ) |
MultiModalProcessorSenderCache (L379) | P0 | サイズメタデータ + prompt_updates | item=None を返す(P1にデータあり、IPC不要) |
ShmObjectStoreSenderCache (L437) | P0 | 共有メモリ参照 + prompt_updates | item=None を返す(共有メモリ経由でP1に渡す) |
MultiModalReceiverCache (L614) | P1 | テンソルデータ | lru タイプ時に P1 側で使用 |
P0-P1 キャッシュの整合性
設計の核心: P0とP1のキャッシュは 同一のEviction順序 を維持する。
is_cached() × N get_and_update()
P0: From API ───────────────────> ────────────────> To P1
get_and_update()
P1: From P0 ───────────────────> To model
is_cached()はP0キャッシュのみを参照(Eviction順序を変えない)get_and_update()は P0 と P1 で順番に呼ぶ必要がある(Eviction順序を同期)- これにより、P0のキャッシュ状態を見るだけでP1のキャッシュ状態を推定できる(IPC不要)
キャッシュヒット時にスキップされる処理
- HF Processor実行(画像のリサイズ、正規化、パッチ分割 →
pixel_valuesテンソル生成) - テンソルデータのIPC送信(
SenderCache/ShmCache使用時、data=NoneにしてZMQ転送量削減) - プロンプト更新の再計算は常に必要(キャッシュにprompt_updatesが保存されているため計算はスキップだが、取得は必要)
5. EngineCoreRequest への組み立て
MultiModalFeatureSpec 構築
参照: target/vllm/vllm/v1/engine/input_processor.py:627-654
mm_features = []
for modality, idx in sorted_mm_idxs:
base_mm_hash = decoder_mm_hashes[modality][idx]
mm_features.append(
MultiModalFeatureSpec(
data=decoder_mm_inputs[modality][idx], # MultiModalKwargsItem | None
modality=modality, # "image"
identifier=_get_mm_identifier(base_mm_hash, lora_request),
mm_position=decoder_mm_positions[modality][idx], # PlaceholderRange
mm_hash=base_mm_hash,
)
)
sorted_mm_idxs は argsort_mm_positions() でプロンプト内の出現順にソートされる。
MultiModalFeatureSpec の構造
参照: target/vllm/vllm/multimodal/inputs.py:337-381
| フィールド | 型 | 説明 |
|---|---|---|
data | MultiModalKwargsItem | None | 処理済みテンソルデータ。P0キャッシュヒット時は None |
modality | str | "image", "video", "audio" 等 |
identifier | str | エンコーダキャッシュ用ハッシュ(LoRAプレフィックス付きの場合あり) |
mm_position | PlaceholderRange | プロンプト内のプレースホルダー位置 |
mm_hash | str | None | プロセッサキャッシュ用ハッシュ(LoRAプレフィックスなし) |
PlaceholderRange の構造
参照: target/vllm/vllm/multimodal/inputs.py:170-240
| フィールド | 型 | 説明 |
|---|---|---|
offset | int | プロンプト内の開始位置 |
length | int | プレースホルダーの長さ(トークン数) |
is_embed | Tensor[bool] | None | 各位置が埋め込みを受け取るかのマスク |
get_num_embeds() は実際のエンコーダ出力の埋め込み数を返す(is_embed のTrue数、またはlength)。
EngineCoreRequest
参照: target/vllm/vllm/v1/engine/__init__.py:55-101
テキスト推論との差分:
mm_features: list[MultiModalFeatureSpec] | None— マルチモーダル時に設定されるprompt_token_idsにはプレースホルダー展開済みのトークン列が入る
ZMQ送信時は msgspec によるバイナリシリアライゼーション。テンソルデータは MultiModalKwargsItem に含まれ、カスタムエンコーダで処理される。
6. キャッシュタイプ別のデータフロー
processor_only(P0完結)
P0: hash → cache miss → HF処理 → cache store(tensor+prompt) → tensor をリクエストに含めて送信
P0: hash → cache hit → cache get(tensor+prompt) → tensor をリクエストに含めて送信
- テンソルデータは常にZMQ経由で送信される
lru(P0 Sender + P1 Receiver)
P0: hash → cache miss → HF処理 → meta store(size+prompt) → tensor をリクエストに含めて送信
P1: hash → cache miss → tensor を受信 → cache store(tensor)
P0: hash → cache hit → meta get(prompt) → data=None で送信(テンソル省略)
P1: hash → cache hit → cache get(tensor)
- キャッシュヒット時は テンソルデータのIPC転送がスキップ される
shm(共有メモリ)
P0: hash → cache miss → HF処理 → 共有メモリに書き込み → data=None で送信
P1: hash → cache miss → 共有メモリから読み取り
P0: hash → cache hit → data=None で送信
P1: hash → cache hit → 共有メモリから読み取り(ringバッファ)
主要ファイル
| ファイル | 主要クラス/関数 | 行 |
|---|---|---|
target/vllm/vllm/v1/engine/input_processor.py | InputProcessor, process_inputs(), _get_mm_identifier() | L56, L521, L490 |
target/vllm/vllm/inputs/preprocess.py | InputPreprocessor, _process_multimodal(), _get_mm_processor() | L60, L193, L182 |
target/vllm/vllm/multimodal/hasher.py | MultiModalHasher, hash_kwargs(), serialize_item() | L50, L154, L52 |
target/vllm/vllm/multimodal/cache.py | MultiModalProcessorOnlyCache, SenderCache, ShmCache, ReceiverCache | L326, L379, L437, L614 |
target/vllm/vllm/multimodal/registry.py | MULTIMODAL_REGISTRY, processor_cache_from_config() | L305 |
target/vllm/vllm/multimodal/inputs.py | MultiModalFeatureSpec, PlaceholderRange | L337, L170 |
target/vllm/vllm/v1/engine/__init__.py | EngineCoreRequest | L55 |
target/vllm/vllm/model_executor/models/gemma3_mm.py | Gemma3MultiModalProcessor, Gemma3ProcessingInfo | L276, L77 |
関連ドキュメント
OutputProcessor
深度: [SHALLOW] 確信度: [VERIFIED] 最終更新: 2026-02-11
概要
OutputProcessorはフロントエンドプロセスで動作し、バックエンド(EngineCore)からZMQ経由で受信したEngineCoreOutputを、ユーザー向けのRequestOutputに変換する。主な処理はインクリメンタルデトークナイズ、停止文字列判定、logprobs処理である。AsyncLLMのoutput_handlerバックグラウンドタスクから呼び出される。
process_outputs() フロー
参照: target/vllm/vllm/v1/engine/output_processor.py:582 (process_outputs)
OutputProcessor.process_outputs(engine_core_outputs) # L582
│
for each engine_core_output:
│
├─ req_state = request_states[req_id] # RequestState取得
│ (abortされていればスキップ)
│
├─ 統計情報更新 # L620-622
│
├─ デトークナイズ + 停止文字列判定 # L637-639
│ stop_string = detokenizer.update(
│ new_token_ids, stop_terminated)
│ → トークン→テキスト変換(インクリメンタル)
│ → 停止文字列検出時は finish_reason = STOP
│
├─ logprobs処理 # L646
│ logprobs_processor.update_from_output(output)
│
├─ RequestOutput構築 # L649-656
│ req_state.make_request_output(
│ new_token_ids, finish_reason, stop_reason, ...)
│ → CompletionOutput + RequestOutput
│
├─ 出力配信 # L660-665
│ ├─ AsyncLLM: req_state.queue.put(request_output)
│ └─ LLM: request_outputs.append(request_output)
│
└─ 完了処理 # L668-687
if finish_reason is not None:
_finish_request(req_state)
→ リクエスト解放、統計記録
RequestState
参照: target/vllm/vllm/v1/engine/output_processor.py:116 (RequestState)
各リクエストのフロントエンド側状態を保持する。OutputProcessor.add_request()で作成される。
| フィールド | 型 | 説明 |
|---|---|---|
external_req_id | str | 外部リクエストID(クライアント向け) |
detokenizer | IncrementalDetokenizer | デトークナイザインスタンス |
logprobs_processor | LogprobsProcessor | logprobs処理インスタンス |
output_kind | RequestOutputKind | 出力モード(CUMULATIVE/DELTA/FINAL_ONLY) |
queue | RequestOutputCollector | None | AsyncLLM用出力キュー |
prompt_token_ids | list[int] | プロンプトトークン(出力に含める用) |
make_request_output()
参照: target/vllm/vllm/v1/engine/output_processor.py:269 (make_request_output)
make_request_output(new_token_ids, finish_reason, ...)
│
├─ FINAL_ONLY モードかつ未完了 → None(出力なし)
│
├─ プーリングモデル → PoolingRequestOutput
│
└─ テキスト生成 → RequestOutput
├─ _new_completion_output() # L377
│ ├─ detokenizer.get_next_output_text(finished, delta)
│ │ → DELTAモード: 新規テキストのみ
│ │ → CUMULATIVEモード: 全テキスト
│ └─ CompletionOutput(text, token_ids, logprobs, ...)
└─ RequestOutput(request_id, outputs, finished, ...)
Detokenizer(インクリメンタルデトークナイズ)
参照: target/vllm/vllm/v1/engine/detokenizer.py:30 (IncrementalDetokenizer)
クラス階層
IncrementalDetokenizer (基底・No-op) L30
└── BaseIncrementalDetokenizer (ABC) L65
├── FastIncrementalDetokenizer L169
│ → HF tokenizersの DecodeStream 使用
└── SlowIncrementalDetokenizer L258
→ detokenize_incrementally() 使用
ファクトリメソッド from_new_request() がトークナイザの種類に応じて適切な実装を選択する。
update() メソッド
参照: target/vllm/vllm/v1/engine/detokenizer.py:65 (BaseIncrementalDetokenizer.update)
update(new_token_ids, stop_terminated) → stop_string | None
│
for each new_token_id:
├─ token_ids.append(new_token_id)
└─ output_text += decode_next(new_token_id) # 抽象メソッド
│
└─ check_stop_strings(output_text, ...) # L316
→ (stop_string, truncate_offset) | None
停止文字列判定
参照: target/vllm/vllm/v1/engine/detokenizer.py:316 (check_stop_strings)
check_stop_strings()は累積テキストの末尾付近で停止文字列を検索する。検出時はテキストをトランケートし、停止文字列と切り詰め位置を返す。include_stop_str_in_outputフラグで停止文字列を出力に含めるか制御する。
LogprobsProcessor
参照: target/vllm/vllm/v1/engine/logprobs.py:28 (LogprobsProcessor)
SamplingParams.logprobs / prompt_logprobs の設定に基づいて初期化される。update_from_output()でEngineCoreOutputからlogprobs情報を抽出し、累積対数確率を更新する。
出力モード(RequestOutputKind)
参照: target/vllm/vllm/sampling_params.py:108 (RequestOutputKind)
| モード | 値 | 動作 | 用途 |
|---|---|---|---|
CUMULATIVE | 0 | 毎回全出力テキスト/トークンを返す | デフォルト |
DELTA | 1 | 差分(新規テキスト/トークン)のみ返す | ストリーミング |
FINAL_ONLY | 2 | 完了時のみ出力を返す | バッチ処理 |
AsyncLLMとの連携
AsyncLLM._run_output_handler() # async_llm.py:662
while True:
outputs = await engine_core.get_output_async() # ZMQ受信
for chunk in outputs.outputs:
output_processor.process_outputs(chunk, ...) # ← ここで呼ばれる
→ RequestOutputがper-requestキューにpush
→ generate()がキューからyield
上流・下流の関係
- 上流: AsyncLLM(output_handlerタスクから呼び出し)、EngineCoreOutputs(ZMQ経由受信)
- 下流: APIサーバー(RequestOutputをyield)
Phase 2 深堀り候補
RequestOutputCollectorのキューイング実装- ストリーミングモード(DELTA)時のテキスト差分計算詳細
- n>1サンプリング時のParentRequest管理
主要ファイル
| ファイル | 主要クラス/関数 |
|---|---|
target/vllm/vllm/v1/engine/output_processor.py | OutputProcessor (L73), process_outputs() (L582), RequestState (L116), make_request_output() (L269) |
target/vllm/vllm/v1/engine/detokenizer.py | IncrementalDetokenizer (L30), FastIncrementalDetokenizer (L169), SlowIncrementalDetokenizer (L258), check_stop_strings() (L316) |
target/vllm/vllm/v1/engine/logprobs.py | LogprobsProcessor (L28) |
target/vllm/vllm/outputs.py | RequestOutput (L86), CompletionOutput (L23) |
target/vllm/vllm/sampling_params.py | RequestOutputKind (L108) |
Scheduler サマリー
深度: [MEDIUM] 確信度: [VERIFIED] 最終更新: 2026-02-11
概要
SchedulerはContinuous Batchingの中核コンポーネントであり、各ステップでどのリクエストにどれだけのトークンを計算させるかを決定する。schedule()メソッドは3フェーズ(RUNNING処理 → WAITING処理 → Output構築)で構成され、トークン予算の範囲内で最大限のリクエストをスケジュールする。Unified Compute Modelを採用し、Prefill/Decodeを明示的に区別せず num_computed_tokens の進捗で統一的に管理する。
アーキテクチャ
graph TD
subgraph schedule 3フェーズ
P1["Phase 1: RUNNING<br>既実行リクエスト処理<br>L350-517"]
P2["Phase 2: WAITING<br>新規リクエスト受け入れ<br>L532-800"]
P3["Phase 3: Output構築<br>SchedulerOutput生成<br>L827-896"]
end
AR["add_request()"] -->|"WAITINGキューに追加"| P2
P1 -->|"トークン予算消費"| P2
P2 -->|"トークン予算消費"| P3
P1 -->|"プリエンプション"| KVM["KVCacheManager"]
P2 -->|"allocate_slots()"| KVM
P2 -->|"get_computed_blocks()"| KVM
P3 -->|"SchedulerOutput"| EC["EngineCore"]
UO["update_from_output()"] -->|"ModelRunnerOutput"| OUT["EngineCoreOutputs"]
主要コンポーネント
| コンポーネント | 用途 | ファイル |
|---|---|---|
Scheduler | スケジューリング本体 | target/vllm/vllm/v1/core/sched/scheduler.py |
Request | リクエスト内部状態 | target/vllm/vllm/v1/request.py |
SchedulerOutput | スケジュール結果(Executor向け) | target/vllm/vllm/v1/core/sched/output.py:184 |
NewRequestData | 初回スケジュールのフルデータ | target/vllm/vllm/v1/core/sched/output.py:34 |
CachedRequestData | 既スケジュール済みの差分データ | target/vllm/vllm/v1/core/sched/output.py:114 |
主要メソッド
| メソッド | 行 | 説明 |
|---|---|---|
schedule() | L321 | メイン: 3フェーズスケジューリング → SchedulerOutput |
add_request() | L1644 | WAITINGキューにリクエスト登録 |
update_from_output() | L1241 | ModelRunnerOutputから出力生成 → EngineCoreOutputs |
finish_requests() | L1666 | リクエストを完了/中止状態にする |
_preempt_request() | L898 | プリエンプション実行(ブロック解放→WAITINGに戻す) |
_make_cached_request_data() | L999 | CachedRequestData(差分データ)構築 |
_update_request_with_output() | L1538 | 生成トークンをリクエストに追加、停止判定 |
schedule() 3フェーズ
Phase 1: RUNNING リクエストのスケジューリング(L350-517)
既に実行中のリクエストに対してトークンを割り当てる。
while req_index < len(self.running) and token_budget > 0:
request = self.running[req_index]
# 計算すべき新規トークン数
num_new_tokens = (
request.num_tokens_with_spec # 最終目標
+ request.num_output_placeholders # asyncプレースホルダ
- request.num_computed_tokens # 既計算分を差引
)
num_new_tokens = min(num_new_tokens, token_budget)
# KVキャッシュブロック割り当て
new_blocks = kv_cache_manager.allocate_slots(request, num_new_tokens)
if new_blocks is None:
# → プリエンプション: 最低優先度リクエストを解放して再試行
プリエンプション: ブロック割り当て失敗時、Priority/FIFOポリシーで最低優先度のリクエストを選びブロック解放。解放後に再試行する。
Phase 2: WAITING リクエストのスケジューリング(L532-800)
WAITINGキューから新規リクエストを受け入れる。
for request in self.waiting:
# スキップ条件チェック
# - WAITING_FOR_REMOTE_KVS: 非同期KV受信待ち
# - WAITING_FOR_FSM: 構造化出力のFSMコンパイル待ち
# - WAITING_FOR_STREAMING_REQ: ストリーミング入力待ち
# - LoRA制約超過
# プレフィックスキャッシュ検索(初回のみ)
if request.num_computed_tokens == 0:
computed_blocks, num_hits = kv_cache_manager.get_computed_blocks(request)
# KVコネクタ(LMCache等)による外部キャッシュも検索
# 計算対象トークン数
num_new_tokens = request.num_tokens - num_computed_tokens
num_new_tokens = min(num_new_tokens, token_budget)
# KVキャッシュブロック割り当て
new_blocks = kv_cache_manager.allocate_slots(request, num_new_tokens, ...)
if new_blocks is None:
break # ← RUNNINGと異なりプリエンプションせずループ終了
# RUNNINGキューに追加
self.running.append(request)
request.status = RequestStatus.RUNNING
token_budget -= num_new_tokens
Phase 3: SchedulerOutput 構築(L827-896)
# 新規リクエスト → NewRequestData(フルデータ)
new_reqs_data = [NewRequestData.from_request(req, block_ids) for req in scheduled_new_reqs]
# 既実行リクエスト → CachedRequestData(差分のみ)
cached_reqs_data = self._make_cached_request_data(running_reqs, resumed_reqs, ...)
return SchedulerOutput(
scheduled_new_reqs=new_reqs_data,
scheduled_cached_reqs=cached_reqs_data,
num_scheduled_tokens=num_scheduled_tokens,
total_num_scheduled_tokens=total,
...
)
Unified Compute Model
vLLM v1のSchedulerはPrefillとDecodeを明示的に区別しない。各リクエストの num_computed_tokens が num_tokens_with_spec(プロンプト長 + 出力長 + スペキュレーショントークン)に追いつくまでトークンを割り当てる。
このアプローチにより以下が統一的に扱える:
- Chunked Prefill: 大きなプロンプトを複数ステップに分割
- Prefix Caching: キャッシュヒット分を
num_computed_tokensに反映 - Speculative Decoding: ドラフトトークンを
num_tokens_with_specに含める
トークン予算
token_budget = self.max_num_scheduled_tokens # ステップあたりの上限
- 各リクエストのスケジュール時に
token_budget -= num_new_tokensで消費 - Phase 1(RUNNING)→ Phase 2(WAITING)の順で消費
- 枯渇時: RUNNING側は continue(次リクエスト試行)、WAITING側は break(ループ終了)
プリエンプション
参照: target/vllm/vllm/v1/core/sched/scheduler.py:898 (_preempt_request)
KVキャッシュブロック不足時にRUNNINGリクエストに対してのみ発動:
- ポリシーに基づき最低優先度のリクエストを選択
- Priority:
(priority, arrival_time)が最大のリクエスト - FIFO: 最後のリクエスト
- Priority:
kv_cache_manager.free(request)でブロック解放request.status = RequestStatus.PREEMPTED、num_computed_tokens = 0にリセット- WAITINGキューの先頭に戻す(LIFO順序で優先再スケジュール)
Request ステータス遷移
stateDiagram-v2
[*] --> WAITING: add_request()
WAITING --> WAITING_FOR_FSM: FSMコンパイル待ち
WAITING --> WAITING_FOR_REMOTE_KVS: リモートKV受信待ち
WAITING --> WAITING_FOR_STREAMING_REQ: ストリーミング入力待ち
WAITING_FOR_FSM --> WAITING: コンパイル完了
WAITING_FOR_REMOTE_KVS --> WAITING: 受信完了
WAITING_FOR_STREAMING_REQ --> WAITING: 入力完了
WAITING --> RUNNING: schedule()で選択
RUNNING --> PREEMPTED: KVキャッシュ不足
PREEMPTED --> WAITING: キュー先頭に戻す
RUNNING --> FINISHED_STOPPED: EOS/stop_token検出
RUNNING --> FINISHED_LENGTH_CAPPED: max_tokens到達
RUNNING --> FINISHED_ABORTED: ユーザーによる中止
RUNNING --> FINISHED_ERROR: エラー発生
update_from_output() フロー
参照: target/vllm/vllm/v1/core/sched/scheduler.py:1241
update_from_output(scheduler_output, model_runner_output)
→ dict[int, EngineCoreOutputs]
処理:
for req_id in scheduler_output.scheduled requests:
# Speculative Decoding リジェクション処理
# → 不採用トークン分 num_computed_tokens を巻き戻し
# 生成トークンをリクエストに追加
new_token_ids, stopped = _update_request_with_output(request, tokens)
# 完了判定
if stopped:
finish_reason = request.get_finished_reason()
_free_request(request) # ブロック解放
# EngineCoreOutput を構築
outputs[client_index].append(EngineCoreOutput(
request_id, new_token_ids, finish_reason, logprobs, ...
))
return {client_index: EngineCoreOutputs(outputs=outs) for ...}
設定
| パラメータ | デフォルト | 説明 |
|---|---|---|
max_num_seqs | 設定依存 | 同時実行リクエスト数上限 |
max_num_batched_tokens | 設定依存 | ステップあたりのトークン予算上限 |
enable_chunked_prefill | 設定依存 | Chunked Prefillの有効化 |
long_prefill_token_threshold | 0 | 長プロンプト分割閾値(0=無効) |
scheduling_policy | Priority | プリエンプション選択ポリシー |
呼び出しフロー
EngineCore.add_request(request)
→ scheduler.add_request(request) # WAITINGキューに登録
EngineCore.step()
├─ scheduler.schedule() # → SchedulerOutput
│ ├─ kv_cache_manager.get_computed_blocks() # プレフィックスキャッシュ
│ └─ kv_cache_manager.allocate_slots() # ブロック割り当て
├─ executor.execute_model(scheduler_output) # GPU実行
└─ scheduler.update_from_output(output) # → EngineCoreOutputs
関連ドキュメント
CacheBlend GitHub 議論調査
調査日: 2026-02-14 対象リポジトリ: vllm-project/vllm, LMCache/LMCache, LMCache/LMCache-Ascend
エグゼクティブサマリー
CacheBlendは、プレフィックス一致に限定せず、部分的・非連続的なKVキャッシュの再利用を可能にする技術(論文: arXiv:2405.16444)。LMCacheプロジェクトがvLLM向けの実装を提供しているが、vLLM本体への統合はまだ実現していない。
最大の未解決課題は オンライン推論(vllm serve)でのCacheBlend利用であり、2026年2月時点でも安定した動作は実現されていない。オフライン(LLM.generate()直接呼び出し)では動作するが、HTTP APIを通じた利用では複数の技術的障壁がある。
主要な議論・論点
1. vLLM本体への Generalized KV Cache Reuse RFC
- Issue: vllm-project/vllm#25950 (open)
- 状態: 停滞中(staleラベル後にunstaleされたが、実装は未着手)
プレフィックス一致に限定しない一般化されたKVキャッシュ再利用をvLLMに組み込む提案。3領域の変更を提案:
- Attention Kernel: per-tokenマスキングパラメータの追加(FlashInferは既にサポート)
- KV Connector: 成功ビットマップの返却(現在のsequentialなprefix長ではなく)
- Scheduler: 穴あきKVキャッシュを持つトークンの適切な処理
2026年1月12日の重要な進展: カーネルやグルーロジックを変更せずに実装する方法を発見。プロンプトを複数のサブリクエストに分割してバッチ内で処理するアプローチ。コードは「soon」とされたが、まだ公開されていない。
LMCacheチーム(@ApostaC)は「CacheBlendは既にLMCacheに実装済み」と返答。提案者(@iddo10)は「LMCacheの実装は別のforwardパスを書いているが、この提案はvLLM本体に組み込む」と差別化を主張。
2. vllm serve でのCacheBlendサポート(オンライン推論) [最重要]
- Issue: LMCache#1936 (open)
- 関連: #1136, #1290, #1682 (いずれもclosed/stale)
- 状態: 未解決。繰り返し報告されており、最も重要な未解決課題
CacheBlendは現在 LLM.generate() によるオフライン推論でのみ動作。vllm serve 経由のHTTP APIでは以下の技術的障壁がある:
- トークン化の不一致:
blend_special_str(例:" # #")が前後のコンテキストにより異なるトークンIDに変換される。HTTP API経由ではトークン化を制御不可能 /v1/chat/completionsはinput_idsを受け付けない: セグメント境界の正確な指定が不可能/v1/completionsでinput_idsを渡してもキャッシュのロードが発生しない: ストアは行われるがリユースなし- vLLM GPUワーカーへのパッチが必要:
gpu_worker.pyの手動修正が必要で、バージョン間で互換性が壊れる
ワークアラウンド(2026年2月3日 @rick-heig): Qwen3ファミリーでは <|file_sep|> のような常に同じトークンIDに変換される特殊トークンをblend区切り文字として使うことで、テキストレベルでの操作が可能になる。
3. CacheBlend V1の品質・安定性バグ
- ガーブル出力 (#2496, open): 複数エントリ処理時、最初以外の出力が文字化け。GPU/CPUメモリのクリア不足の疑い
- キャッシュヒット後の保存漏れ (#2029, open/stale): 部分ヒット時、新規計算トークンが保存されない → 後続リクエストのヒット率低下
- 先頭ミス時の探索打ち切り (#2029): 最初のチャンクがキャッシュミスすると後続チャンクの探索なし → RAGで致命的
- layerwiseモードでのKVキャッシュ破損 (PR#2329コメント, 2026-02-03): layerwise有効時、温度0でも異なる応答
4. LMCache側の安定化PR
- PR#762 (2025-06マージ): CacheBlend V1初期実装
- PR#2329 (open, コンフリクトあり): layerwise/blendingエッジケース修正 + vLLMパッチヘルパー。テスト中にlayerwiseのKVキャッシュ破損が新たに報告されており、マージ見通し不明
5. バージョン互換性
LMCache-Ascendチームが整理した互換性マトリクス (#154):
| vLLM | LMCache | CacheBlend |
|---|---|---|
| 0.9.2 | 0.3.3 / 0.3.7 | Production Ready |
| 0.10.0 | 0.3.7 | Not supported |
| 0.11.0 | 0.3.7 | Not supported |
| 0.10.0 | 0.3.12 | Production Ready |
| 0.11.0 | 0.3.12 | Production Ready |
※ Ascend NPU版マトリクス。GPU版LMCacheとは異なる可能性あり。
タイムライン
| 日付 | イベント |
|---|---|
| 2025-06-07 | CacheBlend V1 初期実装が LMCache にマージ (PR#762) |
| 2025-07-24 | オンライン推論での動作不良が初報告 (#1136) |
| 2025-08-05 | トークン化不一致問題の根本原因が特定 |
| 2025-09-30 | vLLM本体に Generalized KV Cache Reuse RFC提出 (#25950) |
| 2025-10-31 | vllm serve サポートの明確な Feature Request (#1936) |
| 2025-12-29 | layerwise/blending修正PR提出 (PR#2329) |
| 2026-01-12 | vLLM RFC#25950にてサブリクエスト分割アプローチ発見の報告 |
| 2026-01-27 | ガーブル出力バグ報告 (#2496) |
| 2026-02-03 | layerwise KVキャッシュ破損の新規報告 + 特殊トークンワークアラウンド提案 |
結論・所見
オンライン推論(vllm serve)の現状
動作しない。オフライン専用の状態が約8ヶ月続いている。根本的な問題はCacheBlendのセグメント区切りがトークンレベルの精密な制御を要求するのに対し、HTTP APIがテキストレベルの入力しか受け付けない点にある。
2つのアプローチが存在するが、いずれも未完成:
- LMCache側のアプローチ: vLLMのworkerにパッチを当てて対応。バージョン間の互換性維持が困難
- vLLM本体側のアプローチ: RFC#25950のサブリクエスト分割方式。コード未公開。実現すればvLLM本体の機能としてCacheBlendが使えるようになる可能性があるが、不透明
プラグイン開発への示唆
CacheBlendの現状は、vLLMのKV Connectorインターフェースの限界を示している:
- 現在のKV ConnectorはPrefix Caching前提(連続ブロックの転送)
- 非連続キャッシュ再利用にはScheduler・Attention Kernelレベルの変更が必要
- RFC#25950のサブリクエスト分割アプローチが実現すれば、KV Connector層のみで対応可能になる
独自プラグイン作成を検討する場合、CacheBlendの統合方式の行方は重要な参考情報となる。
CacheBlend 実装調査報告 [MEDIUM] [VERIFIED]
最終更新: 2026-02-15 対象ソース:
target/LMCache/lmcache/v1/compute/blend/,target/LMCache/lmcache/v1/compute/models/,target/LMCache/lmcache/v1/compute/attention/,target/LMCache/lmcache/v1/gpu_connector/gpu_connectors.py,target/LMCache/lmcache/integration/vllm/vllm_v1_adapter.py
調査目的
CacheBlendの実装が論文の主張(RAGチャンク間のクロスアテンション、選択的KV再計算、レイヤー別KV選択、KVロード/再計算パイプライン)をどの程度実現しているか、またvLLM本体へのパッチなしにKV Transferプラグインだけで動作するかを明らかにする。
結論
CacheBlendはvLLM本体への ad-hoc パッチが必要。KV Transferプラグインの枠組みだけでは完結しない。理由は、CacheBlendがvLLMのモデルオブジェクト(Transformerレイヤー群)に直接アクセスして独自のforward計算を行うため。
論文機能 vs 実装状況
| 論文の主張 | 実装状態 | 備考 |
|---|---|---|
| RAGチャンク間のKV再利用 | 実装済み | セパレータトークンで段落分割、段落単位でKV保存・ルックアップ |
| 重要token同定(選択的KV再計算) | 実装済み | topk by K差分のL2ノルム。check_layersで指定レイヤーで判定 |
| レイヤー別KV選択 | 部分実装 | check_layersで判定レイヤーを指定可能だが、判定結果は以降の全レイヤーに一律適用 |
| KVロード/再計算パイプライン | 実装済み | ジェネレータでレイヤー別にロード→RoPE補正→paged memory書込みをオーバーラップ |
| レイヤーごとの異なるrecomp_ratio | 未実装 (TODO) | recomp_ratios[0]のハードコード。TODOコメントあり |
| 閾値ベースのblending | 未実装 (TODO) | blend_thresholds設定は存在するが未使用 |
| TP/PP対応 | 未実装 (TODO) | base.py冒頭にTODOコメント |
| マルチモーダル対応 | 未実装 (TODO) | 同上 |
| プレフィックスキャッシュとの互換 | 未実装 (TODO) | vllm_v1_adapter.py:803にTODOコメント |
アーキテクチャ
全体構成
graph TD
subgraph vLLM_Worker["vLLM Worker (パッチ必要)"]
GMR[GPUModelRunner] -->|"model参照を登録"| VMT[VLLMModelTracker]
end
subgraph LMCache_Blend["LMCache CacheBlend"]
VMT -->|"get_model()"| LMB[LMCBlender]
LMB -->|"layerwise_model"| LMM[LMCBaseModel<br/>独自forward実行]
LMM -->|"process_qkv()"| LMB
LMB -->|"get_kv(layer_id)"| GPU_BUF[VLLMBufferLayerwiseGPUConnector<br/>中間バッファ]
GPU_BUF -->|"batched_to_gpu()"| KVC[vLLM KVCache<br/>paged memory]
end
subgraph LMCache_Storage["LMCache Storage"]
CE[LMCacheEngine] -->|"retrieve_layer()"| GPU_BUF
CE -->|"チャンク取得"| SM[StorageManager<br/>CPU/Disk/Remote]
end
LMB -->|"blend()"| CE
CacheBlendの実行フロー
LMCacheConnectorV1Impl.start_load_kv() 内で self.blender.blend() が呼ばれる:
- セパレータ分割: 入力トークン列をRAGチャンク区切り(
" # # "など)で段落に分割 - 外部KVロード: 段落単位でLMCacheストレージからKVキャッシュを取得 → 中間GPUバッファに配置
- RoPE位置補正: 保存時の位置エンコーディングを新しい位置に変換(FusedRope カスタムCUDAカーネル)
- 独自forward実行: vLLMモデルのレイヤー群を使って入力を再計算
- 重要token同定:
check_layersレイヤーで新旧Kの差分L2ノルムを計算、topk個を選択 - 選択的アテンション再計算: 選択されたtokenのみでattentionを再計算(FlashAttn or FlashInfer sparse)
- KVマージ: 古いKV(外部ストレージ由来)に重要tokenのKVのみ上書き
- paged memory書込み: 最終KVをvLLMのpaged memoryに書き込み
参照: target/LMCache/lmcache/v1/compute/blend/blender.py:59-120
重要token同定アルゴリズム
# blender.py:88-110 (process_qkv)
if layer_id in self.common_metadata.check_layers:
# 新しいK(再計算)と古いK(ストレージ由来)のL2距離
diff_k = torch.sum(
(k.to(torch.float32) - old_k.to(torch.float32)) ** 2, dim=[1]
)
# recomp_ratio割合のtop-kを選択
topk_num = int(total_len * self.common_metadata.recomp_ratios[0])
top_indices = torch.topk(diff_k, k=topk_num).indices
top_indices, _ = torch.sort(top_indices)
# 選択されたtokenのみQ, K, Vを抽出
k, v = k[top_indices], v[top_indices]
q = q[top_indices]
# 古いKVの該当位置に新しいKVを書き込み
old_k[self.metadata.imp_indices] = k
old_v[self.metadata.imp_indices] = v
参照: target/LMCache/lmcache/v1/compute/blend/blender.py:88-120
独自モデルforward(LMCBaseModel.compute_layer)
CacheBlendはvLLMのnormalなforward pathを使わず、vLLMのモデルオブジェクトのレイヤーを直接操作する独自のforwardを実行する:
# base.py:66-141 (compute_layer, ジェネレータ)
@torch.compile
def compute_layer(self, input_ids):
hidden_states = self.vllm_model.get_input_embeddings(input_ids)
for idx, layer in enumerate(self.vllm_model.model.layers[...]):
# input_layernorm
hidden_states, residual = layer.input_layernorm(hidden_states, residual)
# QKV projection
qkv, _ = layer.self_attn.qkv_proj(hidden_states)
q, k, v = qkv.split([q_size, kv_size, kv_size], dim=-1)
# モデル固有QKV処理(Qwen3のq_norm/k_norm等)
q, k, v = self._process_qkv(q, k, v, layer)
# ★ blenderによる重要token選択・KVマージ
q, k, v, residual, attn_output, attn_metadata = self.blender.process_qkv(...)
# 独自attentionバックエンド(flash_attn_varlen_func)
attn_output = self.lmc_attn_layers[idx].forward_contiguous(q, k, v, ...)
# output projection + MLP
hidden_states, _ = layer.self_attn.o_proj(attn_output)
hidden_states, residual = layer.post_attention_layernorm(hidden_states, residual)
hidden_states = layer.mlp(hidden_states)
yield # レイヤーごとにジェネレータで制御を返す
重要: これはvLLMの通常のforward pathとは完全に別のコードパス。vLLMのAttention層、CUDAGraph、InputBatch最適化等は一切使われない。
参照: target/LMCache/lmcache/v1/compute/models/base.py:66-141
対応モデル
| モデル | クラス | QKV処理 |
|---|---|---|
| Llama | LMCLlamaModel | なし(そのまま) |
| Qwen2 | LMCLlamaModel(流用) | なし(そのまま) |
| Qwen3 | LMCQwen3Model | q_norm/k_norm 適用 |
3モデルのみ。新モデル追加には LMCBaseModel の継承と _process_qkv() 実装が必要。
参照: target/LMCache/lmcache/v1/compute/models/utils.py:14-35
アテンションバックエンド
| バックエンド | クラス | 用途 |
|---|---|---|
| FlashAttention | LMCFlashAttnBackend | 密なアテンション計算(デフォルト) |
| FlashInfer Sparse | HackBSAWrapper + LMCFlashInferSparseMetadata | スパースブロックアテンション(enable_sparse=True時) |
enable_sparse=True を指定すると、FlashInferのVariableBlockSparseAttentionWrapperをハックした HackBSAWrapper を使用して、選択されたtokenブロックのみスパースにアテンション計算を行う。
参照: target/LMCache/lmcache/v1/compute/attention/flash_attn.py, target/LMCache/lmcache/v1/compute/attention/flash_infer_sparse.py
vLLM本体パッチの分析
必要なパッチ(examples/blend_kv_v1/README.md に明記)
# vllm/v1/worker/gpu_worker.py の load_model() 末尾に追加が必要:
from lmcache.v1.compute.models.utils import VLLMModelTracker
from lmcache.integration.vllm.utils import ENGINE_NAME
VLLMModelTracker.register_model(ENGINE_NAME, self.model_runner.model)
ensure_kv_transfer_initialized(self.vllm_config)
さらに init_worker_distributed_environment() 内の ensure_kv_transfer_initialized() をコメントアウトし、上記の load_model() に移動する必要がある。
参照: target/LMCache/examples/blend_kv_v1/README.md
パッチが必要な理由
- モデルオブジェクトへの直接アクセス:
VLLMModelTrackerにvLLMのモデルオブジェクトを登録する必要がある。CacheBlendはこのオブジェクトの.model.layers[i]に直接アクセスして独自forwardを実行する - 初期化順序の変更: KV Transfer初期化をモデルロード後に移動する必要がある(モデルオブジェクトが存在しないとblender構築に失敗)
- KVConnectorのAPIでは不十分: KVConnectorBase_V1の
start_load_kv()/wait_for_layer_load()/save_kv_layer()インタフェースは、KVの読み書きのみを想定。CacheBlendが行う「独自forward+選択的再計算」はスコープ外
プラグイン境界の問題
| KV Transferプラグインの範囲 | CacheBlendが必要とすること | ギャップ |
|---|---|---|
| KVキャッシュの読み書き | モデルのforward計算 | forwardはWorker/GPUModelRunnerの責任 |
| レイヤー別KVロード/セーブ | レイヤー内部のQKV取得・attention再計算 | レイヤー内部ロジックへのアクセス不可 |
| メタデータ受け渡し | vLLMモデルオブジェクトへの参照 | メタデータでは渡せない |
| slot_mapping操作 | 独自のattention計算結果のpaged memory書込み | 通常のattention pathをバイパス |
VLLMBufferLayerwiseGPUConnector — Blending専用GPUコネクタ
enable_blending=True 時に選択される専用コネクタ。通常のLayerwiseGPUConnectorとの違い:
- 中間GPUバッファ: ロード結果をpaged memoryではなく連続バッファに保持(
get_kv(layer_id)でblenderがアクセス) - RoPE位置補正:
batched_to_gpu()ジェネレータ内でfused_rotary_embによる位置エンコーディング変換を実行 - ギャップゼロイング: RAGチャンク間のセパレータ位置をゼロで埋める(
current_gap_positions) - ダブルバッファ: compute/loadバッファのping-pongでロードと計算をオーバーラップ
パイプライン動作
Layer 0: [ロード: CPU→GPU buf_load]
[compute: ─────────────] [RoPE補正: buf_load→buf_compute]
Layer 1: [ロード: CPU→GPU buf_load]
[compute: ─────────────] [RoPE補正: buf_load→buf_compute] [buf→paged: Layer 0]
Layer 2: [ロード: CPU→GPU buf_load]
[compute: ─────────────] [RoPE補正: buf_load→buf_compute] [buf→paged: Layer 1]
...
Layer N+1: [buf→paged: Layer N-1]
Layer N+2: (yield完了)
参照: target/LMCache/lmcache/v1/gpu_connector/gpu_connectors.py:552-836
BlendServer(マルチプロセス版)
blend_server.py は MPCacheEngine を継承した BlendEngine クラスを提供する。ZMQメッセージキューで別プロセスとして動作し、以下の追加APIを持つ:
| API | 用途 |
|---|---|
cb_register_kv_cache | blendエンジンのKVキャッシュ登録 |
cb_lookup_pre_computed | セパレータで段落分割→段落単位ルックアップ |
cb_store_pre_computed | 段落単位の事前計算KV保存(BLEND_HASH_PREFIX付き) |
cb_retrieve_pre_computed | 段落単位の事前計算KV取得→GPUバッファ書込み |
cb_store_final | 最終KV保存(通常ハッシュ、通常モードLLMからも利用可能) |
ParallelPatternMatcher(Cで実装)でセパレータトークンを高速マッチング。
参照: target/LMCache/lmcache/v1/multiprocess/blend_server.py
設定
LMCache側
| 設定 | デフォルト | 用途 |
|---|---|---|
enable_blending | False | CacheBlendモード有効化 |
blend_check_layers | None | 重要token判定を行うレイヤーIDリスト |
blend_recompute_ratios | None | 再計算するtokenの割合リスト(現在は[0]のみ使用) |
blend_thresholds | None | 閾値ベースblending用(未実装) |
enable_sparse (extra_config) | False | FlashInfer スパースアテンション使用 |
参照: target/LMCache/lmcache/v1/config.py:101-118
暗黙の前提
use_layerwise=Trueが必須(blending有効時に自動選択:VLLMBufferLayerwiseGPUConnector)save_unfull_chunk=Trueが自動設定されるSegmentTokenDatabaseが使用される(ChunkedTokenDatabaseではなく)
制約・未実装機能
- vLLM本体パッチ必須 — プラグインのみでは動作しない
- 対応モデル3種のみ — Llama, Qwen2, Qwen3。新モデルはLMCBaseModel継承が必要
- RoPE制約 —
rotary_dim == head_size、rope_scaling=None、partial_rotary_factor=1.0のみ - プレフィックスキャッシュ非互換 — TODOコメント、blending後のプレフィックスキャッシュスキップ
- TP/PP未対応 — TODOコメント
- マルチモーダル未対応 — TODOコメント
- バッチサイズ1前提 — FlashAttnMetadata初期化で
batch_size=1ハードコード - レイヤー別ratio未実装 —
recomp_ratios[0]のみ使用 - CUDAGraph未対応 — 独自forward pathのため
torch.compileのみ
関連ドキュメント
- LMCache統合調査報告 — LMCache全体のvLLM統合
- KV Transfer summary — KVConnectorBase_V1のAPI
- GPUModelRunner summary — 通常のforward path
ECConnector GitHub 議論調査レポート
ステータス: 調査完了 作成日: 2026-02-14 深度: [MEDIUM] 確信度: [VERIFIED] 関連ドキュメント: EncoderCache 永続化と階層キャッシュ、KVCacheManager
1. 概要
ECConnector は、マルチモーダルモデルの Encode-Prefill-Decode (EPD) 分離推論 を実現するために導入されたインフラストラクチャである。エンコーダ出力(画像埋め込みなど)を別プロセス間で転送・キャッシュするための抽象インタフェース ECConnectorBase を定義し、プラグイン可能な設計になっている。
2025年11月に基盤PR (#25233) がマージされた後、encoder-onlyモード、リモートキャッシュチェック最適化、ec_both ロールが順次マージされた。現在は高性能転送バックエンド(SHMConnector vs Mooncake ECConnector)の方向性が議論中で、メンテナーは Mooncake ベースの統一的なソリューションを志向している。
2. マージ済み PR(確定した設計)
2.1 基盤: EPD分離 (#25233) — 2025-11-12 マージ [VERIFIED]
主導: Chenguang Zheng (fake0fan)
ECConnectorBase はスケジューラ側とワーカー側の責務を明確に分離:
| 責務 | メソッド | 実行場所 |
|---|---|---|
| キャッシュ存在確認 | check_caches_exist | スケジューラ |
| 割当後状態更新 | update_state_after_alloc | スケジューラ |
| メタデータ構築 | build_connector_meta | スケジューラ |
| リクエスト完了通知 | request_finished | スケジューラ |
| キャッシュ読み込み | start_load_caches | ワーカー |
| キャッシュ保存 | save_caches | ワーカー |
参照実装として ECExampleConnector(safetensors でディスク保存)を同梱。ec_connector_module_path によるOOTプラグインロードもサポート。
参照: target/vllm/vllm/distributed/ec_transfer/ec_connector/
2.2 Encoder-only モード (#30242) — 2025-12-22 マージ [VERIFIED]
EPD分離時にエンコーダ専用インスタンスがLLM重みまでロードしてしまう問題を解決。--convert mm_encoder_only オプションを追加。
- 当初
VisionOnlyという名称 → オーディオ等も対象のためMMEncoderOnlyに変更 - A100でのGPUメモリ使用量: 44GB → 3.7GB に削減
adapters.pyの pooling モデルと同様のアプローチ
2.3 リモートキャッシュチェック最適化 (#32585) — 2026-01-22 マージ [VERIFIED]
スケジューラでのリモートエンコーダキャッシュ存在確認を最適化。EPD追跡RFCのタスクリストの一環。
2.4 ec_both ロール (#34182) — 2026-02-10 マージ [VERIFIED]
貢献: Qi Wang (furionw, NVIDIA)
従来は producer/consumer のどちらか一方のみだったが、集約ノード(EPD一体型)でのエンベディングオフロード(GPU → CPU → GPU)を可能にするため ec_both ロールを追加。KV Connector の both ロールと同様の概念。変更は3ファイル・計10行程度の小規模PR。
3. 進行中の議論
3.1 高性能転送バックエンド: SHMConnector vs Mooncake [INFERRED]
関連 PR: #33714 (open)
PiratePai が共有メモリ + PyTorch RPC ベースの SHMConnector を提案:
- RTX 4090 での1E-1PD構成で ExampleConnector より 8-9% の TTFT 改善 を実証
しかしメンテナー (NickLucche) は「vllm のツリー内コネクタは少数の高品質ソリューションに絞りたい」と慎重姿勢。fake0fan は Mooncake ベースの統一 ECConnector を提案し、RDMA/NVLink/TCP フォールバックの複数バックエンドを1つのコネクタで対応する構想を示している。
技術的課題: エンコーダキャッシュの固定アドレス問題
エンコーダキャッシュテンソルは KVキャッシュと異なり事前割り当てされた固定アドレスを持たない。NickLucche は2つの選択肢を提示:
- 中間バッファ方式: 転送前にコピーする
- 事前割り当て方式: ECConnector 使用時にはエンコーダキャッシュを固定アドレスバッファに切り替える(現在の dict ベースを置き換え)
3.2 NIXL/セグメントツリーベースのEPD分離 (#26009) [SHALLOW]
H. Jhoo (MerHS) が NIXL コミュニケータを使った P2P 直接通信方式を提案。セグメントツリーベースのエンコーダキャッシュマネージャ(ベストフィット割り当て)、動的ロールスイッチング、エンコーダ専用スケジューラなど大規模な変更を含む。
- 90日間の非活動で stale クローズ(2026-02-07)
- fake0fan の fork に NIXL ECConnector の作業が継続中
3.3 マルチモーダル前処理の重複排除 (#27094) [INFERRED]
EPD分離時に全ワーカーで画像前処理が繰り返される問題:
- プロファイリングで前処理が推論時間の 約50%(Qwen2.5-VL-3B で 143ms × 3回 ≈ 430ms)を占める
image_metaタイプの導入を提案し、エンコーダ処理後は形状メタデータのみを後続ワーカーに転送する案- DarkLight1337 は前処理をエンコーダプロセスに完全移動する方針を示唆
3.4 ECキャッシュの解放メカニズム (#32659) [SHALLOW]
EPD追跡RFCのタスクの一つ。エンコーダキャッシュの明示的解放メカニズムが未実装で、リソースリークの原因となりうる。fake0fan の fork で作業中。
4. タイムライン
gantt
title ECConnector 開発タイムライン
dateFormat YYYY-MM-DD
section 基盤
初期EPD PR (#21740) :done, 2025-09-01, 2025-09-19
EPD基盤 (#25233) :done, 2025-09-19, 2025-11-12
section 機能拡張
NIXL方式提案 (#26009) :done, 2025-10-01, 2026-02-07
Encoder-only (#30242) :done, 2025-12-01, 2025-12-22
キャッシュチェック最適化 (#32585) :done, 2026-01-15, 2026-01-22
ec_both ロール (#34182) :done, 2026-02-03, 2026-02-10
section 進行中
EPD追跡RFC (#32659) :active, 2026-01-20, 2026-03-01
SHMConnector (#33714) :active, 2026-02-03, 2026-03-01
MM前処理重複排除 (#27094) :active, 2025-10-17, 2026-03-01
5. 未解決の課題一覧
| 課題 | Issue/PR | 状態 | 備考 |
|---|---|---|---|
| 高性能転送バックエンド | #33714 | 方向性議論中 | Mooncake統一案が有力 |
| ECキャッシュ解放 | #32659 | 作業中 | fake0fan fork |
| EPDプロキシ最適化 | #31017 | 未着手 | — |
| MM前処理重複排除 | #27094 | RFC | 前処理の50%コスト |
| エンコーダキャッシュ事前割り当て | #33714 コメント | 提案段階 | dict→固定バッファ |
| NIXL ECConnector | #32659 タスク | fork作業中 | — |
6. 主要コントリビューター
| 名前 | GitHub | 役割 |
|---|---|---|
| Chenguang Zheng | fake0fan | ECConnector基盤設計者、EPDフォローアップ主導 |
| NickLucche | NickLucche | vLLM Collaborator、アーキテクチャ方針決定者 |
| Cyrus Leung | DarkLight1337 | vLLM Member、マルチモーダルレビューアー |
| Qi Wang | furionw (NVIDIA) | ec_both ロール貢献 |
| H. Jhoo | MerHS | NIXL方式提案者 |
| PiratePai | PiratePai | SHMConnector 実装者 |
7. プラグイン開発への示唆 [INFERRED]
独自ECConnectorプラグインを開発する場合の留意点:
- 現在の安定インタフェース:
ECConnectorBaseの6メソッドは安定しているが、今後のリファクタリング(事前割り当て方式への移行)で変更の可能性あり - OOTプラグイン:
ec_connector_module_pathで外部モジュールロード可能。ECConnectorFactory経由で登録 - 転送方式の選択: Mooncake方式が統一バックエンドとして推奨される方向。独自実装よりMooncakeの上に構築する方が将来的に有利
- エンコーダキャッシュ管理の変更予定: dict ベースから事前割り当て型への移行が検討中。プラグインはこの変更に備えるべき
EncoderCache 永続化と階層キャッシュ: 調査報告
ステータス: 調査完了 — ECConnector 既存インフラの発見により設計方針が確定 作成日: 2026-02-14 深度: [MEDIUM] 確信度: [VERIFIED] 関連ドキュメント: Gemma3 ビジョンパイプライン: キャッシュ機構、マルチモーダル バックエンド MM 処理
1. 背景と動機
現状の EncoderCache
vLLM の EncoderCache は、ビジョンエンコーダ(例: SiglipVisionModel + Projector)の GPU 上の出力テンソルをキャッシュする。Gemma3 27B の場合、出力形状は (N×256, 5376) で、1 画像あたり約 2.6 MB(FP16)。
| 項目 | 現状 |
|---|---|
| 格納先 | GPU メモリ(gpu_model_runner.encoder_cache) |
| キャッシュキー | mm_hash or {lora_name}:{mm_hash} |
| Eviction 方式 | FIFO(OrderedDict.popitem(last=False)) |
| 容量設定 | encoder_cache_size(エンベディング数単位) |
| 永続性 | なし(プロセス終了で消失) |
| 管理 | EncoderCacheManager(CPU 側論理管理)+ encoder_cache dict(GPU 側物理格納) |
参照: target/vllm/vllm/v1/core/encoder_cache_manager.py (EncoderCacheManager)、target/vllm/vllm/v1/worker/gpu_model_runner.py:439 (encoder_cache dict)
RAG ユースケースにおける課題
RAG では同一ドキュメント画像が異なるクエリから繰り返し参照される。
- FIFO Eviction との相性の悪さ: 高頻度アクセス画像でも新しいエントリが来れば古い順に追い出される
- GPU メモリの有限性:
encoder_cache_sizeを大きくしてもデコーダ KV Cache と競合 - 再起動耐性がない: vLLM プロセス再起動で全キャッシュが消失
エンコーダ処理のコスト
EncoderCache がスキップする GPU 上の処理:
- SiglipVisionModel: Conv2d + position_embedding + 27 層 Transformer Encoder(双方向 Attention)+ post_layernorm
- Gemma3MultiModalProjector: AvgPool2d + GemmaRMSNorm + Linear(1152→5376)
- split + flatten
RAG コーパスが数千〜数万画像規模の場合、毎回の再計算コストは無視できない。
2. 重要な発見: ECConnector 既存インフラ
KV Transfer ではなく ECConnector が正解
当初の仮説では「KV Transfer の枠組み(LMCache 等)を活用」する方針だったが、調査の結果、エンコーダキャッシュの外部ストレージ永続化のために設計された専用インフラ「ECConnector」が既に存在することが判明した。
ECConnector の全体像
target/vllm/vllm/distributed/ec_transfer/
__init__.py -- get_ec_transfer(), has_ec_transfer()
ec_transfer_state.py -- グローバルシングルトン管理
ec_connector/
__init__.py
base.py -- ECConnectorBase (抽象基底クラス)
factory.py -- ECConnectorFactory (プラグイン登録)
example_connector.py -- ECExampleConnector (参照実装, 199行)
参照: target/vllm/vllm/distributed/ec_transfer/ec_connector/base.py:59 (ECConnectorBase)
ECConnectorBase の抽象メソッド [VERIFIED]
ECConnectorBase は KVConnectorBase_V1 とは完全に独立した抽象基底クラス。
| メソッド | 分類 | 説明 |
|---|---|---|
start_load_caches(encoder_cache, **kwargs) | Worker | 外部ストレージから encoder_cache dict にテンソルをロード |
save_caches(encoder_cache, mm_hash, **kwargs) | Worker | encoder_cache から外部ストレージにテンソルを保存 |
has_cache_item(identifier) | Scheduler | 外部ストレージにキャッシュが存在するか確認 |
update_state_after_alloc(request, index) | Scheduler | アロケーション後の内部状態更新 |
build_connector_meta(scheduler_output) | Scheduler | Scheduler → Worker 間のメタデータ構築 |
参照: target/vllm/vllm/distributed/ec_transfer/ec_connector/base.py:126-224
ECConnector の統合ポイント [VERIFIED]
既にGPUModelRunnerとSchedulerに統合済み:
Scheduler 側 (target/vllm/vllm/v1/core/sched/scheduler.py:1197-1203):
if self.ec_connector is not None and self.ec_connector.has_cache_item(
item_identifier
):
mm_hashes_to_schedule.add(item_identifier)
external_load_encoder_input.append(i)
num_embeds_to_schedule += num_encoder_embeds
continue
→ ECConnector にキャッシュが存在する場合、エンコーダ計算バジェットを消費せず、ロード予約のみ行う。
Worker 側 (target/vllm/vllm/v1/worker/gpu_model_runner.py:2444-2445):
self.encoder_cache[mm_hash] = output
self.maybe_save_ec_to_connector(self.encoder_cache, mm_hash)
→ エンコーダ実行後、結果を GPU dict に格納すると同時に ECConnector にも保存。
Worker 側コンテキストマネージャ (target/vllm/vllm/v1/worker/ec_connector_model_runner_mixin.py:62-85):
ec_connector.bind_connector_metadata(scheduler_output.ec_connector_metadata)
if not ec_connector.is_producer:
ec_connector.start_load_caches(encoder_cache, **kwargs)
try:
yield output # _execute_mm_encoder() + _gather_mm_embeddings() が実行される
finally:
output.finished_sending, output.finished_recving = (
ec_connector.get_finished(scheduler_output.finished_req_ids)
)
ec_connector.clear_connector_metadata()
→ start_load_caches() でストレージからロード → エンコーダ実行(ロード済みはスキップ)→ 完了通知。
ECTransferConfig [VERIFIED]
参照: target/vllm/vllm/config/ec_transfer.py
| フィールド | 型 | デフォルト | 説明 |
|---|---|---|---|
ec_connector | str | None | None | コネクタ名(例: "ECExampleConnector") |
ec_role | ECRole | None | None | "ec_producer" or "ec_consumer" |
ec_connector_extra_config | dict | {} | コネクタ固有の追加設定 |
ec_connector_module_path | str | None | None | 動的ロード用モジュールパス |
engine_id | str | None | uuid4 自動生成 | エンジンID |
起動時パラメータ例: --ec-connector ECExampleConnector --ec-role ec_producer --ec-connector-extra-config '{"shared_storage_path": "/mnt/cache"}'
ECConnectorFactory [VERIFIED]
参照: target/vllm/vllm/distributed/ec_transfer/ec_connector/factory.py
register_connector(name, module_path, class_name)で遅延ロード登録ec_connector_module_pathによる動的ロード(外部モジュール対応)- 現在の登録済みコネクタ:
ECExampleConnectorのみ
3. ECExampleConnector 参照実装の分析 [VERIFIED]
参照: target/vllm/vllm/distributed/ec_transfer/ec_connector/example_connector.py
概要
safetensors を使ってディスクにエンコーダ出力テンソルを保存/読込するデバッグ用実装。全 199 行。
ストレージ構造
{shared_storage_path}/
{mm_hash}/
encoder_cache.safetensors
保存 (save_caches, L98-118)
def save_caches(self, encoder_cache, mm_hash, **kwargs) -> None:
if not self.is_producer:
return
filename = self._generate_filename_debug(mm_hash)
ec_cache = encoder_cache[mm_hash]
tensors = {"ec_cache": ec_cache.detach().cpu()} # GPU→CPU コピー
safetensors.torch.save_file(tensors, filename)
- Producer ロールの場合のみ保存
detach().cpu()で GPU テンソルを CPU に移動してからシリアライズ- テンソル形状に一切依存しない
ロード (start_load_caches, L63-96)
def start_load_caches(self, encoder_cache, **kwargs) -> None:
metadata = self._get_connector_metadata()
for mm_data in metadata.mm_datas:
if mm_data.mm_hash in encoder_cache:
continue # 既に GPU dict にあればスキップ
filename = self._generate_filename_debug(mm_data.mm_hash)
ec_cache = safetensors.torch.load_file(filename, device=...)["ec_cache"]
encoder_cache[mm_data.mm_hash] = ec_cache # dict に直接格納
- メタデータ(Scheduler が構築)に基づいてロード対象を決定
encoder_cachedict に直接格納 →_gather_mm_embeddings()でそのまま読める
存在チェック (has_cache_item, L120-133)
def has_cache_item(self, identifier: str) -> bool:
return self._found_match_for_mm_data(identifier)
# → os.path.exists(filename)
- ファイルの存在確認のみ(同期的)
メタデータ管理
ECExampleConnectorMetadata (L35-42): ロードすべき mm_hash と num_token のリスト。
update_state_after_alloc() (L135-146): Scheduler が allocate() 後に呼び出し、_mm_datas_need_loads にロード対象を追加。
build_connector_meta() (L148-164): _mm_datas_need_loads からメタデータを構築し、Worker に伝達。呼び出し後にクリア。
4. KV Transfer との比較 [VERIFIED]
| 評価項目 | KV Transfer | ECConnector |
|---|---|---|
| 設計目的 | デコーダ KV Cache の転送・永続化 | エンコーダ出力テンソルの転送・永続化 |
| テンソル粒度 | レイヤー別、ブロック単位、トークン粒度 | mm_hash 単位、任意形状テンソル |
| テンソル形状依存 | あり (num_layer, 2, chunk_size, num_kv_heads, head_size) | なし |
| エンコーダ出力への適合性 | 不適合 | 最適 |
| 既存統合ポイント | Attention 層デコレータ経由 | GPUModelRunner の _execute_mm_encoder 直後 |
| 新規実装量 | 大(7 abstract メソッド + KV 概念適合) | 小(5 abstract メソッド、参照実装 199 行) |
| ストレージ実装 | LMCache/NIXL/Mooncake(全て KV 前提) | Example(safetensors/ディスク)、拡張容易 |
結論: エンコーダ出力テンソルの永続化には ECConnector を使うべき。KV Transfer はデコーダ KV Cache に特化しており、エンコーダ出力の形状・粒度に合わない。
LMCache の KV 形状ハードコード箇所:
target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/lmcache_integration/vllm_v1_adapter.py:477kv_shape = (num_layer, 1 if use_mla else 2, chunk_size, num_kv_heads, head_size)
5. FIFO → LRU 変更の具体的設計
現状の FIFO 実装 [VERIFIED]
参照: target/vllm/vllm/v1/core/encoder_cache_manager.py
データ構造:
# L72-77
self.cached: dict[str, set[str]] = {} # mm_hash → {request_ids}
self.freeable: OrderedDict[str, int] = OrderedDict() # mm_hash → num_embeds (挿入順)
self.freed: list[str] = [] # evict 済みリスト
FIFO の核心 (L173-177):
while num_embeds > self.num_free_slots:
mm_hash, num_free_embeds = self.freeable.popitem(last=False) # 最も古いエントリから
del self.cached[mm_hash]
self.freed.append(mm_hash)
self.num_free_slots += num_free_embeds
OrderedDict.popitem(last=False) で最初に挿入された(=最も早く参照解放された)エントリから Evict。
現状の FIFO が LRU と異なる点
FIFO は「最も早く freeable に追加されたものから Evict」する。LRU は「最も長期間アクセスされていないものから Evict」する。
差が出るケース:
- 画像 A が freeable に入る(参照解放)
- 画像 B が freeable に入る
- 画像 A が再度参照される → freeable から取り出されて active に戻る
- 画像 A が再度 freeable に入る → FIFO: 末尾に追加(最新扱い)、LRU: 末尾に追加(最新扱い)
実は、現状の FIFO は「参照解放順」であり、再参照された画像は freeable の末尾に再挿入されるため、RAG での繰り返しアクセスパターンでは擬似 LRU として機能する部分もある。
しかし、active 状態(参照中)のエントリ間でのアクセス頻度は考慮されない。複数リクエストが同時に異なる画像を参照し、それらが一斉に freeable になった場合、「最後にアクセスされた時刻」ではなく「最後に参照解放された時刻」で順序が決まる。
LRU への変更方法
変更箇所: encoder_cache_manager.py の 1 ファイルのみ。Scheduler 側・GPUModelRunner 側の変更は不要。
方法 A: アクセス時刻の追跡(推奨)
class EncoderCacheManager:
def __init__(self, cache_size: int):
# ... 既存フィールド ...
self._access_order: dict[str, int] = {} # mm_hash → monotonic counter
self._access_counter: int = 0
def check_and_update_cache(self, request, input_id) -> bool:
mm_hash = request.mm_features[input_id].identifier
if mm_hash not in self.cached:
return False
if not self.cached[mm_hash]:
num_encoder_embeds = self.freeable.pop(mm_hash)
self.num_freeable_slots -= num_encoder_embeds
self.cached[mm_hash].add(request.request_id)
# ★ アクセス時刻を更新
self._access_counter += 1
self._access_order[mm_hash] = self._access_counter
return True
def free_encoder_input(self, request, input_id) -> None:
req_id = request.request_id
mm_hash = request.mm_features[input_id].identifier
if not self.cached.get(mm_hash, None):
return
self.cached[mm_hash].discard(req_id)
if not self.cached[mm_hash]:
num_encoder_embeds = request.get_num_encoder_embeds(input_id)
self.freeable[mm_hash] = num_encoder_embeds
self.num_freeable_slots += num_encoder_embeds
# ★ アクセス時刻でソートされた位置に挿入
# OrderedDictを再ソート: 古いアクセスが先頭に来るようにする
self.freeable.move_to_end(mm_hash) # 末尾に追加(最新アクセス)
def allocate(self, request, input_id) -> None:
mm_hash = request.mm_features[input_id].identifier
# ... 既存ロジック ...
# ★ アクセス時刻を記録
self._access_counter += 1
self._access_order[mm_hash] = self._access_counter
方法 B: 簡易 LRU(move_to_end のみ)
現状の実装でも、freeable への再挿入は末尾に行われるため、ほぼ LRU として機能する。唯一の改善点は、check_and_update_cache() で freeable から復活する際のタイムスタンプ更新のみ。実質的に方法 A と同等の効果が得られる。
変更の影響範囲
| コンポーネント | 変更 |
|---|---|
encoder_cache_manager.py | check_and_update_cache() と free_encoder_input() の 2 メソッド修正 |
can_allocate() | 変更不要(popitem(last=False) は同じ) |
| Scheduler | 変更不要(API は同じ) |
| GPUModelRunner | 変更不要 |
6. 階層キャッシュの実装設計
アーキテクチャ
リクエスト到着
│
▼
Scheduler: _try_schedule_encoder_inputs()
│
├── check_and_update_cache() → L1 HIT (GPU dict) → スキップ
│
├── L1 MISS → ec_connector.has_cache_item() → L2 HIT (Storage)
│ │
│ └── external_load_encoder_input に追加 → Worker でロード予約
│
└── L1/L2 MISS → encoder_inputs_to_schedule に追加 → エンコーダ計算
│
▼
Worker: execute_model()
│
├── start_load_caches() → L2 からテンソルを GPU dict にロード
│
├── _execute_mm_encoder() → L1/L2 MISS 分のみエンコーダ実行
│ └── save_caches() → 新規計算結果を L2 に保存
│
└── _gather_mm_embeddings() → GPU dict からテンソル取得
2 層キャッシュの役割分担
| L1: GPU dict(ホット) | L2: ECConnector(コールド) | |
|---|---|---|
| 格納先 | GPU メモリ | Redis / ディスク / NFS 等 |
| 容量 | 小(encoder_cache_size) | 大(コーパス全体) |
| レイテンシ | ナノ秒 | マイクロ〜ミリ秒 |
| Eviction | LRU(提案変更後) | TTL or LRU or なし |
| 永続性 | なし | あり |
| 管理 | EncoderCacheManager | カスタム ECConnectorBase 実装 |
カスタム ECConnector の実装ガイド
新しい ECConnector を作成するには、ECConnectorBase を継承して 5 つの abstract メソッドを実装する。
# my_ec_connector.py
from vllm.distributed.ec_transfer.ec_connector.base import (
ECConnectorBase, ECConnectorMetadata, ECConnectorRole
)
class RedisECConnector(ECConnectorBase):
def __init__(self, vllm_config, role):
super().__init__(vllm_config=vllm_config, role=role)
self._redis_url = vllm_config.ec_transfer_config.get_from_extra_config(
"redis_url", "redis://localhost:6379"
)
# Redis クライアント初期化...
# Worker側: ストレージからGPU dictにロード
def start_load_caches(self, encoder_cache, **kwargs):
metadata = self._get_connector_metadata()
for mm_data in metadata.mm_datas:
if mm_data.mm_hash in encoder_cache:
continue
tensor_bytes = self._redis.get(mm_data.mm_hash)
if tensor_bytes:
encoder_cache[mm_data.mm_hash] = deserialize(tensor_bytes)
# Worker側: GPU dictからストレージに保存
def save_caches(self, encoder_cache, mm_hash, **kwargs):
if not self.is_producer:
return
tensor = encoder_cache[mm_hash].detach().cpu()
self._redis.set(mm_hash, serialize(tensor))
# Scheduler側: ストレージにキャッシュが存在するか
def has_cache_item(self, identifier):
return self._redis.exists(identifier)
# Scheduler側: アロケーション後の状態更新
def update_state_after_alloc(self, request, index):
mm_hash = request.mm_features[index].identifier
num_token = request.get_num_encoder_embeds(index)
self._need_loads[mm_hash] = num_token
# Scheduler側: メタデータ構築
def build_connector_meta(self, scheduler_output):
meta = MyECConnectorMetadata()
for mm_hash, num_token in self._need_loads.items():
meta.add(mm_hash, num_token)
self._need_loads.clear()
return meta
登録方法:
- ファクトリ登録:
ECConnectorFactory.register_connector("RedisECConnector", "my.module", "RedisECConnector") - または動的ロード:
--ec-connector RedisECConnector --ec-connector-module-path my.module
テンソルサイズの見積もり
Gemma3 27B、1 画像あたり:
256 tokens × 5376 dim × 2 bytes (FP16) = 2,752,512 bytes ≈ 2.6 MB/画像
| コーパス規模 | L2 ストレージ必要量(FP16) |
|---|---|
| 1,000 画像 | ≈ 2.6 GB |
| 10,000 画像 | ≈ 26 GB |
| 100,000 画像 | ≈ 260 GB |
プリコンピュート運用
ECConnector を活用したオフラインプリコンピュートの流れ:
- Producer モードで vLLM を起動し、コーパス全画像を含むダミーリクエストを送信
save_caches()でエンコーダ出力がストレージに蓄積される- Consumer モードで本番 vLLM を起動
- リクエスト到着時に
has_cache_item()→start_load_caches()でストレージからロード - エンコーダ計算をスキップし、ストレージからの読み出し + GPU 転送のみで処理
7. 残る設計上の考慮事項
7.1 ECExampleConnector の同期 I/O
現在の ECExampleConnector の start_load_caches() は同期的な safetensors.torch.load_file() を呼ぶ。ディスク I/O がブロッキングとなり、エンコーダ実行前のレイテンシに直接影響する。
対策案:
start_load_caches()を非同期化(別スレッドでロード開始、_gather_mm_embeddings()前に完了待ち)- Redis 等のインメモリストレージを使い、I/O レイテンシを最小化
- EngineCore.step() のスケジューリングとモデル実行の間の時間的ギャップを活用
7.2 LRU とストレージ Eviction の相互作用
L1(GPU dict)から LRU で Evict されたテンソルは、L2(ストレージ)には残る。次にアクセスされた時:
- Scheduler:
check_and_update_cache()→ L1 MISS - Scheduler:
ec_connector.has_cache_item()→ L2 HIT - Worker:
start_load_caches()→ L2 から L1 にロード
→ エンコーダ再計算は不要だが、ストレージ→GPU 転送のレイテンシが発生する。
7.3 Producer/Consumer ロールの運用
ECConnector は P/D 分離を想定した設計。RAG ユースケースでは:
- ec_producer: プリコンピュート用インスタンス(エンコーダ出力をストレージに書き込み)
- ec_consumer: 本番サービング用インスタンス(ストレージからロード)
- Producer と Consumer で同じストレージパスを共有する必要がある
7.4 キャッシュ無効化
モデル重み更新(LoRA ホットスワップ等)時:
- L1:
EncoderCacheManager.reset()+encoder_cache.clear()で対応済み - L2: ストレージ側のキャッシュクリアが必要(
identifierに LoRA プレフィックスが含まれるため、LoRA 別に無効化可能)
8. 次のステップ
- FIFO→LRU の実装:
encoder_cache_manager.pyの 2 メソッドを修正(変更量: 数行) - カスタム ECConnector の実装: Redis バックエンドの ECConnector を作成(参照: ECExampleConnector の 199 行)
- ベンチマーク: RAG ワークロードでの比較
- ベースライン: FIFO + インメモリのみ
- 改善 1: LRU + インメモリのみ
- 改善 2: LRU + Redis ECConnector
- コミュニティ調査: vLLM の Issue/PR で ECConnector 関連の議論を確認
Gemma3 ビジョンパイプライン: キャッシュ機構 [MEDIUM] [VERIFIED]
最終更新: 2026-02-11
gemma3-vision-pipeline.md で追跡した Gemma3 27B ビジョンパイプライン上には、3つの独立したキャッシュ層が存在する。各キャッシュは異なるステップの重い処理をスキップし、同一画像の再利用や同一プロンプトの再送時に大幅な計算量削減を実現する。
関連: EncoderCache の永続化・階層キャッシュ化については encoder-cache-persistence.md を参照。
1. パイプラインとキャッシュの位置関係
Step 1: API Request
│
Step 2: chat_template 適用
│
┌────────────┴────────────────┐
│ Step 3: Gemma3Processor │
│ (CPU, P0 フロントエンド) │
│ │
│ 3a. image_processor │
│ resize(896×896) │
│ rescale(×1/255) │ ◀── ProcessorCache ヒット時
│ normalize(0.5, 0.5) │ Step 3 全体をスキップ
│ Pan-and-Scan crop │
│ 3b. num_crops 取得 │
│ 3c. プロンプト書き換え │
│ 3d. boi→full_image_seq 展開 │
│ 3e. tokenize │
│ 3f. token_type_ids 生成 │
└────────────┬────────────────┘
│
pixel_values: (N, 3, 896, 896)
prompt_token_ids, mm_hashes
│
═══════════════╪═══════════════ CPU → GPU (ZMQ IPC)
│
┌────────────┴────────────────┐
│ Step 4: SiglipVisionModel │
│ (GPU, P1 バックエンド) │
│ Conv2d → 4096 patches │
│ + position_embedding │ ◀── EncoderCache ヒット時
│ SiglipEncoder × 27層 │ Step 4+5+6 をスキップ
│ post_layernorm │
├───────────────────────────────┤
│ Step 5: Projector │
│ AvgPool2d(k=4) → 256 tokens │
│ GemmaRMSNorm │
│ Linear(1152→5376) │
├───────────────────────────────┤
│ Step 6: split + flatten │
└────────────┬────────────────┘
│
encoder output: (N×256, 5376)
│
┌────────────┴────────────────┐
│ Step 7: embed_input_ids │
│ text_embeds × normalizer │
│ masked_scatter_(mm_embeds) │ ◀── KVプレフィックスキャッシュ ヒット時
├───────────────────────────────┤ プレフィックス一致分の
│ Step 8: Gemma3 Decoder │ Step 7+8 をスキップ
│ 62層 Transformer │ (KV再計算不要)
└──────────────────────────────┘
2. キャッシュ比較テーブル
| ProcessorCache | EncoderCache | KVプレフィックスキャッシュ | |
|---|---|---|---|
| 場所 | CPU (P0 フロントエンド) | GPU (P1 バックエンド) | GPU (P1 バックエンド) |
| キャッシュキー | blake3(model_id, 画像ピクセル, processor_kwargs, tokenizer_kwargs) | mm_feature.identifier (= mm_hash or {lora}:{mm_hash}) | hash(parent_hash, token_ids, extra_keys) — extra_keysにidentifier含む |
| 保存される値 | HF処理済みテンソル (pixel_values, num_patches) + prompt_updates | エンコーダ出力テンソル (post-Projector, GPU上) | デコーダ各層のKV状態 (KVCacheブロック) |
| ヒット時にスキップ | Step 3全体 (CPU前処理) | Step 4+5+6 (エンコーダ+プロジェクタ) | Step 7+8の一部 (プレフィックス分のデコーダ) |
| Eviction方式 | LRU (サイズベース) | FIFO (RefCount管理) | LRU (ブロック単位) |
| 容量設定 | mm_processor_cache_gb (default: 4GB) | encoder_cache_size (埋め込み数単位) | KVCacheの一部 (BlockPool管理) |
| 管理クラス | MultiModalProcessorOnlyCache 等4種 | EncoderCacheManager + encoder_cache dict | KVCacheManager (prefix_cache) |
3. ProcessorCache — CPU側前処理キャッシュ
ハッシュ計算
参照: target/vllm/vllm/multimodal/hasher.py:50-162, target/vllm/vllm/multimodal/processing/processor.py:1299-1363
MultiModalHasher.hash_kwargs(
model_id=model_id, # モデル識別子(例: "google/gemma-3-27b-it")
image=PIL_Image, # 画像データ
**hf_processor_mm_kwargs, # HF Processorへの追加引数
**tokenization_kwargs, # トークナイザ設定
)
ハッシュに投入されるデータ:
| 入力 | シリアライズ方法 |
|---|---|
model_id (str) | UTF-8エンコード |
image (PIL.Image) | EXIF ImageID (UUID型) → 16バイト。なければ mode + ピクセルデータ (numpy配列) |
image (MediaWithBytes) | EXIF ImageID → 16バイト。なければ original_bytes |
hf_processor_mm_kwargs (dict) | キーソート → 再帰的シリアライズ |
tokenization_kwargs (dict) | 同上 |
- ハッシュアルゴリズム:
VLLM_MM_HASHER_ALGORITHM環境変数で設定(blake3デフォルト、sha256/sha512はFIPS準拠用) - キーはアルファベット順にソートされてから逐次ハッシュに投入される(決定的)
参照: target/vllm/vllm/multimodal/hasher.py:154-162 (hash_kwargs)
保存される情報
- テンソルデータ:
pixel_values(形状:(N, 3, 896, 896)),num_patches(形状:(num_images,)) - prompt_updates: プレースホルダー位置情報、展開パターン
参照: target/vllm/vllm/multimodal/cache.py:326-725
CPU/GPU
CPU。P0フロントエンドプロセスのメモリ上で管理される。
スキップされる処理
Step 3 全体(Gemma3Processor.__call__()):
- 3a:
image_processor— resize(896×896), rescale(×1/255), normalize(mean=0.5, std=0.5), Pan-and-Scan時のクロップ生成 - 3b:
num_crops取得 - 3c: プロンプト書き換え(Pan-and-Scan時のみ)
- 3d:
boi_token→full_image_sequence展開 - 3e: tokenizer による
token_ids変換 - 3f:
token_type_ids生成
さらに、Sender/Shm タイプ使用時は ZMQ IPC でのテンソルデータ転送もスキップ される(data=None で送信)。
参照: target/vllm/vllm/multimodal/processing/processor.py:1513-1596 (_cached_apply_hf_processor)
キャッシュフロー詳細
_cached_apply_hf_processor():
1. _hash_mm_items() → MultiModalHashes(各画像のblake3ハッシュ)
2. _get_cache_missing_items() → 各画像がキャッシュにあるか判定
3. _apply_hf_processor_main() → キャッシュミスの画像だけHF Processor実行
4. _merge_mm_kwargs() → キャッシュ済み + 新規処理の結果をマージ
※ マージ前に全ハッシュを touch() して LRU Eviction を防止
参照: target/vllm/vllm/multimodal/processing/processor.py:1365-1400 (_get_cache_missing_items)
4種の実装
| 実装 | 用途 | 格納先 | ヒット時の動作 |
|---|---|---|---|
MultiModalProcessorOnlyCache | P0完結(IPC無効時) | P0メモリ | テンソル+prompt返却 |
MultiModalProcessorSenderCache | P0→P1(LRUモード) | P0にメタデータのみ | data=Noneで送信、IPC転送省略 |
ShmObjectStoreSenderCache | P0→P1(共有メモリ) | 共有メモリ | 共有メモリ参照を返却 |
MultiModalReceiverCache | P1側(LRUモード) | P1メモリ | P0と同期したLRUでテンソル取得 |
参照: target/vllm/vllm/multimodal/registry.py:284-320 (キャッシュタイプ選択ロジック)
4. EncoderCache — GPUエンコーダ出力キャッシュ
キャッシュキー
参照: target/vllm/vllm/v1/engine/input_processor.py:490-506
identifier = mm_hash # 通常
identifier = f"{lora_name}:{mm_hash}" # LoRA tower connector有効時
mm_hash は ProcessorCache と同じ blake3 ハッシュ値。LoRA が有効な場合は、同一画像でも LoRA によってエンコーダ出力が変わるため、LoRA名をプレフィックスとして付加する。
保存される情報
- GPU上のテンソル: SiglipVisionModel + Gemma3MultiModalProjector の出力
- Gemma3の場合:
(N×256, 5376)— Projector出力をflattenしたもの
- Gemma3の場合:
- 論理管理:
EncoderCacheManagerが RefCount + FIFO で管理 - 物理格納:
gpu_model_runner.encoder_cache: dict[str, torch.Tensor]
参照: target/vllm/vllm/v1/core/encoder_cache_manager.py:17-267, target/vllm/vllm/v1/worker/gpu_model_runner.py:439
CPU/GPU
GPU。エンコーダ出力テンソルはGPUメモリ上に保持される。論理管理(RefCount、Eviction判定)はCPU上の EncoderCacheManager が行う。
スキップされる処理
Step 4 + Step 5 + Step 6:
- Step 4: SiglipVisionModel forward — Conv2d(3→1152) + position_embedding + 27層 Transformer Encoder + post_layernorm
- Step 5: Gemma3MultiModalProjector forward — reshape + AvgPool2d(k=4, s=4) + GemmaRMSNorm + Linear(1152→5376)
- Step 6: split + flatten — num_patchesに基づく分割と結合
これらはGPU上で最も計算量の大きいビジョン処理であり、特に SiglipEncoder の 27層の双方向 Attention が支配的。
Scheduler連携
参照: target/vllm/vllm/v1/core/sched/scheduler.py:1060-1215
Scheduler._get_encoder_budget():
1. 各 mm_feature について:
2. encoder_cache_manager.check_and_update_cache(req, i) を呼ぶ
→ True: scheduled_encoder_inputs に含めない(スキップ)
→ False: can_allocate() → allocate() → scheduled_encoder_inputs に追加
3. SchedulerOutput.scheduled_encoder_inputs = {req_id: [input_ids]}
GPUModelRunner 側:
_execute_mm_encoder():
→ scheduled_encoder_inputs にあるもののみ model.embed_multimodal() 実行
→ 出力を encoder_cache[mm_hash] に格納
_gather_mm_embeddings():
→ 全ての mm_feature について encoder_cache[mm_hash] からスライス取得
→ キャッシュヒットしたものも、ミスして今回計算したものも、同じキャッシュから取得
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:2293-2447 (_execute_mm_encoder), target/vllm/vllm/v1/worker/gpu_model_runner.py:2449-2527 (_gather_mm_embeddings)
5. KVプレフィックスキャッシュ — デコーダKV状態キャッシュ
ブロックハッシュ計算
参照: target/vllm/vllm/v1/core/kv_cache_utils.py:525-552
BlockHash(
hash_function((parent_block_hash, curr_block_token_ids_tuple, extra_keys))
)
extra_keys は以下の要素の結合:
extra_keys = lora_extra_keys + mm_extra_keys + cache_salt_keys + prompt_embeds_keys
参照: target/vllm/vllm/v1/core/kv_cache_utils.py:487-522 (generate_block_hash_extra_keys)
MM extra keys の生成
参照: target/vllm/vllm/v1/core/kv_cache_utils.py:387-448
MMトークン(<image> token_id=262144)を含むブロックでは、そのブロックに重なる mm_feature.identifier が extra_keys に追加される。
ブロック [start_token_idx, end_token_idx) が
mm_feature の [offset, offset+length) と重なる場合:
→ extra_keys.append(mm_feature.identifier)
これにより:
- 同一テキスト・異なる画像 → 異なるブロックハッシュ → キャッシュミス
- 同一テキスト・同一画像 → 同一ブロックハッシュ → キャッシュヒット
保存される情報
- GPUメモリ上のKVCacheブロック: デコーダ62層分のKey/Value状態
- BlockPool が物理ブロックを管理、prefix_cache がハッシュ→ブロック対応を管理
CPU/GPU
GPU。KV状態はGPUメモリ上のブロックに格納される。ハッシュ計算とブロック対応管理はCPU上の KVCacheManager が行う。
スキップされる処理
Step 7 + Step 8 の一部(プレフィックスが一致するトークン分):
- Step 7: embed_input_ids — テキスト埋め込み × normalizer + masked_scatter_(mm_embeds)
- Step 8: Gemma3 Decoder forward — 62層 Transformer の KV 計算
プレフィックスキャッシュがヒットすると num_computed_tokens が増加し、新規に forward pass が必要なトークン数が減少する。例えば 1000 トークンのプロンプトで 800 トークン分のプレフィックスがヒットすれば、残り 200 トークンだけ計算すればよい。
参照: target/vllm/vllm/v1/core/kv_cache_manager.py:164-204 (get_computed_blocks)
6. キャッシュの独立性と相互作用
3つのキャッシュは独立に動作する。各キャッシュのヒット/ミスは他のキャッシュの判定に影響しない。
典型シナリオ
シナリオ1: 初回リクエスト(全ミス)
画像A + "この画像は何?" → 全ステップ実行
ProcessorCache: MISS → Step 3 実行、結果をキャッシュ
EncoderCache: MISS → Step 4+5+6 実行、結果をキャッシュ
KV Prefix: MISS → Step 7+8 全トークン実行、KVブロック格納
シナリオ2: 同一画像・同一プロンプト再送(全ヒット)
画像A + "この画像は何?"(2回目)
ProcessorCache: HIT → Step 3 スキップ(pixel_values をキャッシュから取得)
EncoderCache: HIT → Step 4+5+6 スキップ(エンコーダ出力をGPUキャッシュから取得)
KV Prefix: HIT → Step 7+8 のプレフィックス分スキップ(KV状態再利用)
シナリオ3: 同一画像・異なるプロンプト
画像A + "この画像を要約して"
ProcessorCache: HIT → Step 3 スキップ(同一画像なのでハッシュ一致)
EncoderCache: HIT → Step 4+5+6 スキップ(同一 identifier)
KV Prefix: 部分HIT → 画像トークン部分(ブロック単位)はヒットする可能性あり
テキスト部分は異なるためミス
シナリオ4: 異なる画像(全ミス)
画像B + "この画像は何?"
ProcessorCache: MISS → ピクセルデータが異なるためハッシュ不一致
EncoderCache: MISS → identifier が異なる
KV Prefix: MISS → extra_keys の identifier が異なりブロックハッシュ不一致
キャッシュ間のキー共有
3つのキャッシュは同一の mm_hash(blake3ハッシュ)を基盤として共有している:
MultiModalHasher.hash_kwargs(model_id, image, kwargs...)
│
▼
mm_hash (blake3 hex digest)
│
├──▶ ProcessorCache のキー(そのまま使用)
│
├──▶ EncoderCache のキー(= identifier = mm_hash or lora:mm_hash)
│
└──▶ KV Prefix Cache の extra_keys の一部(= identifier)
7. 主要ファイル参照
| ファイル | 主要クラス/関数 | 行 |
|---|---|---|
target/vllm/vllm/multimodal/hasher.py | MultiModalHasher, hash_kwargs(), serialize_item() | L50, L154, L52 |
target/vllm/vllm/multimodal/cache.py | MultiModalProcessorOnlyCache, SenderCache, ShmCache, ReceiverCache | L326, L379, L437, L614 |
target/vllm/vllm/multimodal/processing/processor.py | _cached_apply_hf_processor(), _hash_mm_items(), _get_cache_missing_items() | L1513, L1299, L1365 |
target/vllm/vllm/multimodal/registry.py | processor_cache_from_config() | L305 |
target/vllm/vllm/v1/engine/input_processor.py | _get_mm_identifier() | L490 |
target/vllm/vllm/v1/core/encoder_cache_manager.py | EncoderCacheManager, check_and_update_cache() | L17, L91 |
target/vllm/vllm/v1/worker/gpu_model_runner.py | encoder_cache, _execute_mm_encoder(), _gather_mm_embeddings() | L439, L2293, L2449 |
target/vllm/vllm/v1/core/kv_cache_utils.py | _gen_mm_extra_hash_keys(), generate_block_hash_extra_keys(), hash_block_tokens() | L387, L487, L525 |
target/vllm/vllm/v1/core/kv_cache_manager.py | get_computed_blocks() | L164 |
target/vllm/vllm/v1/core/sched/scheduler.py | _get_encoder_budget() | L1060 |
関連ドキュメント
- Gemma3 ビジョンパイプライン: 形状フローと数値まとめ
- フロントエンド MM処理パス
- バックエンド MM処理パス
- KVCacheManager — プレフィックスキャッシュの詳細
- KVCacheManager: プレフィックスキャッシュ
Gemma3 27B ビジョンパイプライン: 形状フローと数値まとめ
モデルパラメータ(config.json + preprocessor_config.json)
| パラメータ | 値 | 出典 |
|---|---|---|
| image_size | 896 | vision_config |
| patch_size | 14 | vision_config |
| vision hidden_size | 1152 | vision_config |
| vision num_heads | 16 | vision_config |
| vision num_layers | 27 | vision_config |
| text hidden_size | 5376 | text_config |
| text num_heads | 32 | text_config |
| text num_layers | 62 | text_config |
| mm_tokens_per_image | 256 | config.json |
| image_token_index | 262144 | config.json |
| boi_token_index | 255999 | config.json |
| eoi_token_index | 256000 | config.json |
導出値
| 導出パラメータ | 計算 | 値 |
|---|---|---|
| patches_per_image | 896 / 14 | 64 |
| エンコーダ入力パッチ数 | 64² | 4096 |
| tokens_per_side | √256 | 16 |
| AvgPool2d kernel_size | 64 / 16 | 4 |
| Projector 出力トークン/画像 | 16² | 256 (= mm_tokens_per_image ✅) |
Pan-and-Scan 設定
| パラメータ | preprocessor_config.json | フォールバックデフォルト | 出典 |
|---|---|---|---|
| do_pan_and_scan | null | False | processing_gemma3.py L44 |
| pan_and_scan_min_crop_size | null | 256 | processing_gemma3.py L45 |
| pan_and_scan_max_num_crops | null | 4 | processing_gemma3.py L46 |
| pan_and_scan_min_ratio_to_activate | null | 1.2 | processing_gemma3.py L47 |
- Google はモデル配布時にこれらをすべて null にしている
- デフォルト値は HF transformers の
Gemma3ProcessorKwargs._defaultsで定義 - デフォルトでは Pan-and-Scan は無効
- 有効化: vLLM では
--hf-overrides '{"do_pan_and_scan": true}'
API リクエストからデコーダ入力までの全体フロー
Step 1: ユーザーの API リクエスト
{
"model": "gemma-3-27b-it",
"messages": [
{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}},
{"type": "text", "text": "この文書を要約して"}
]
}
]
}
ユーザーは画像を1枚渡すだけ。クロップの存在を意識する必要はない。
Step 2: chat_template 適用
vLLM が chat_template を適用してプロンプト文字列を生成:
<start_of_turn>user
<start_of_image>この文書を要約して<end_of_turn>
<start_of_turn>model
<start_of_image> は boi_token (token_id=255999)。この時点ではプレースホルダが 1個だけ。
Step 3: Gemma3Processor.call() — CPU 側前処理
3a: image_processor による画像前処理
image_inputs = self.image_processor(images, **output_kwargs["images_kwargs"])
画像をリサイズ・正規化し、Pan-and-Scan が有効ならクロップも生成する。
3b: num_crops の取得
num_crops = to_py_obj(image_inputs.pop("num_crops"))
3c: プロンプトの自動書き換え(Pan-and-Scan 時のみ)
for num, idx in reversed(list(zip(num_crops, image_indexes))):
if num: # num=0 なら falsy → この書き換えは発生しない
formatted_image_text = (
f"Here is the original image {self.boi_token} "
f"and here are some crops to help you see better "
+ " ".join([self.boi_token] * num)
)
prompt = prompt[:idx] + formatted_image_text + prompt[idx + len(self.boi_token):]
Pan-and-Scan 無効(num=0)時: if num: が falsy なので、書き換えは一切発生しない。
<start_of_image> は1個のまま次のステップへ。
Pan-and-Scan 有効(num=2)時: 1個の <start_of_image> が以下に置き換えられる:
Here is the original image <start_of_image> and here are some crops to help you see better <start_of_image> <start_of_image>
3d: boi_token → full_image_sequence への展開
self.full_image_sequence = f"\n\n{boi_token}{image_token * 256}{eoi_token}\n\n"
# = "\n\n<start_of_image><image>×256<end_of_image>\n\n"
text = [prompt.replace(self.boi_token, self.full_image_sequence) for prompt in text]
全ての <start_of_image> がそれぞれ 256個の <image> トークンを含む full_image_sequence に展開される。
3e: tokenizer で token_ids に変換
text_inputs = self.tokenizer(text=text, **output_kwargs["text_kwargs"])
<image> トークン (token_id=262144) が並んだ input_ids が生成される。
3f: token_type_ids の生成
mm_token_type_ids[array_ids == self.image_token_id] = 1
# → <image> トークン位置が 1、それ以外が 0
ケース1: デフォルト(Pan-and-Scan 無効)
入力例: A4 150dpi 画像 (1240 × 1754 pixel)
プロンプト変換の流れ
ユーザー入力:
画像1枚 + "この文書を要約して"
chat_template 適用後:
"...<start_of_image>この文書を要約して..."
↑
boi_token 1個
do_pan_and_scan=False → num_crops=0 → プロンプト書き換えなし
boi_token → full_image_sequence 展開後:
"...\n\n<start_of_image><image>×256<end_of_image>\n\nこの文書を要約して..."
↑ ↑×256 ↑
255999 262144 256000
tokenize 後の input_ids (概念的):
[..., 255999, 262144, 262144, ...(×256)..., 262144, 256000, ..., テキスト, ...]
CPU 側前処理
元画像 (1240×1754)
│ resize(896×896, bilinear) ← アスペクト比無視の正方形リサイズ
│ rescale(× 1/255) ← [0,255] → [0,1]
│ normalize(mean=0.5, std=0.5) ← [0,1] → [-1,1]
▼
pixel_values: (1, 3, 896, 896)
num_patches: tensor([1])
GPU 側: SiglipVisionModel
(1, 3, 896, 896)
│ Conv2d(3 → 1152, kernel=14, stride=14)
▼
(1, 1152, 64, 64) ← 896/14 = 64
│ flatten + transpose
▼
(1, 4096, 1152) ← 64² = 4096 パッチ
│ + position_embedding(4096, 1152)
▼
(1, 4096, 1152)
│ SiglipEncoder × 27層
│ (双方向 Attention, heads=16, 4096トークン間全対全)
▼
(1, 4096, 1152)
│ post_layernorm
▼
(1, 4096, 1152)
GPU 側: Gemma3MultiModalProjector
(1, 4096, 1152)
│ transpose → (1, 1152, 4096)
│ reshape → (1, 1152, 64, 64) ← 2Dグリッドに復元
│
│ AvgPool2d(kernel_size=4, stride=4)
▼
(1, 1152, 16, 16) ← 64/4 = 16
│ flatten(2) → (1, 1152, 256)
│ transpose → (1, 256, 1152) ← 16² = 256 トークン
│
│ GemmaRMSNorm(1152)
▼
(1, 256, 1152)
│
│ matmul(mm_input_projection_weight) ← shape: (1152, 5376)
▼
(1, 256, 5376) ← text hidden_size 空間
GPU 側: split + flatten
(1, 256, 5376)
│ split by num_patches=[1] → [(1, 256, 5376)]
│ flatten(0, 1)
▼
(256, 5376) ← 最終出力
GPU 側: テキスト埋め込みとマージ
text_embeds = embed_tokens(input_ids) * normalizer # (seq_len, 5376)
# token_id=262144 は vocab 外 → handle_oov_mm_token=True でゼロ埋め
# is_multimodal: (seq_len,) ← 256箇所が True
merged = masked_scatter_(text_embeds, is_multimodal, mm_embeds) # (256, 5376)
# → 262144 だった256箇所をビジョン埋め込みで上書き
# ※ ビジョン埋め込みには normalizer スケーリングは適用されない
→ (seq_len, 5376) として Gemma3 Decoder (62層) へ
消費トークン数: 256
ケース2: Pan-and-Scan 有効
入力例: 同じ A4 150dpi 画像 (1240 × 1754 pixel)
プロンプト変換の流れ
ユーザー入力:
画像1枚 + "この文書を要約して"
chat_template 適用後:
"...<start_of_image>この文書を要約して..."
↑
boi_token 1個
do_pan_and_scan=True → ratio=1754/1240≈1.415 > 1.2 → num_crops=2
Processor がプロンプトを自動書き換え (Step 3c):
"...Here is the original image <start_of_image> and here are
some crops to help you see better <start_of_image> <start_of_image>
この文書を要約して..."
↑ ↑ ↑
original 用 crop 0 用 crop 1 用
boi_token → full_image_sequence 展開後:
"...Here is the original image \n\n<boi><image>×256<eoi>\n\n and here are
some crops to help you see better \n\n<boi><image>×256<eoi>\n\n
\n\n<boi><image>×256<eoi>\n\nこの文書を要約して..."
tokenize 後:
[..., "Here", "is", ...,
255999, 262144×256, 256000, ← original
..., "and", "here", ...,
255999, 262144×256, 256000, ← crop 0
...,
255999, 262144×256, 256000, ← crop 1
..., テキスト, ...]
CPU 側: Pan-and-Scan 判定
# 縦長画像 (height > width)
ratio = 1754 / 1240 ≈ 1.415
min_ratio_to_activate = 1.2
1.415 > 1.2 → ✅ 発動
CPU 側: クロップ数計算
# 縦長パス (image_height > image_width)
num_crops_h = min(
floor(1754 / 256), # = 6 ← min_crop_size 制約
floor(1754 / 1240 + 0.5), # = 1 ← アスペクト比近似
)
# → min(6, 1) = 1
num_crops_h = max(2, 1) = 2 # 最低2クロップに強制
num_crops_h = min(4, 2) = 2 # max_num_crops でクリップ
num_crops_w = 1
# クロップサイズ検証
crop_size_w = ceil(1240 / 1) = 1240
crop_size_h = ceil(1754 / 2) = 877
min(1240, 877) = 877 > 256 (min_crop_size) → ✅ 有効
結果: 1 × 2 = 2 クロップ
CPU 側: クロップ切り出し + リサイズ
元画像 (1240×1754)
├── original (1240×1754) → resize(896×896) → normalize → (3, 896, 896)
├── crop 0 (1240×877) → resize(896×896) → normalize → (3, 896, 896)
└── crop 1 (1240×877) → resize(896×896) → normalize → (3, 896, 896)
│ stack
pixel_values: (3, 3, 896, 896)
num_patches: tensor([3])
GPU 側: SiglipVisionModel
(3, 3, 896, 896)
│ Conv2d(3 → 1152, kernel=14, stride=14)
▼
(3, 1152, 64, 64)
│ flatten + transpose
▼
(3, 4096, 1152) ← 3枚 × 4096パッチ
│ + position_embedding
▼
(3, 4096, 1152)
│ SiglipEncoder × 27層(双方向 Attention)
▼
(3, 4096, 1152)
GPU 側: Gemma3MultiModalProjector
(3, 4096, 1152)
│ → reshape → (3, 1152, 64, 64)
│ AvgPool2d(k=4, s=4)
▼
(3, 1152, 16, 16)
│ flatten + transpose
▼
(3, 256, 1152)
│ RMSNorm → matmul(1152 → 5376)
▼
(3, 256, 5376)
GPU 側: split + flatten
(3, 256, 5376)
│ split by num_patches=[3] → [(3, 256, 5376)]
│ flatten(0, 1)
▼
(768, 5376) ← 3 × 256 = 768 トークン
GPU 側: テキスト埋め込みとマージ
input_ids 中の token_id=262144 が768箇所
↓ masked_scatter_ で (768, 5376) を順番に書き込み
→ (seq_len, 5376) として Gemma3 Decoder へ
消費トークン数: 768 (= 256 × 3)
プロンプト比較
| Pan-and-Scan 無効(デフォルト) | Pan-and-Scan 有効 | |
|---|---|---|
| プロンプト書き換え | なし | “Here is the original image … crops …” 挿入 |
| boi_token 数 | 1 | 1 + num_crops (= 3) |
<image> トークン数 | 256 | 256 × (1 + num_crops) = 768 |
| 装飾テキスト | なし | “Here is the original image”, “and here are some crops to help you see better” |
| pixel_values shape | (1, 3, 896, 896) | (3, 3, 896, 896) |
| num_patches | tensor([1]) | tensor([3]) |
全体データフロー図
┌─────────────────────────────┐
│ OpenAI 互換 API リクエスト │
│ 画像1枚 + テキスト │
└────────────┬────────────────┘
│
┌────────────┴────────────────┐
│ chat_template 適用 │
│ → "<start_of_image>テキスト" │
│ boi_token(255999) が1個 │
└────────────┬────────────────┘
│
┌────────────┴────────────────┐
│ CPU: Gemma3Processor │
│ │
│ image_processor: │
│ resize(896×896) │
│ rescale(×1/255) │
│ normalize(0.5, 0.5) │
│ Pan-and-Scan 時はクロップ生成│
│ │
│ do_pan_and_scan? │
│ ├── False: │
│ │ 書き換えなし │
│ │ boi_token 1個のまま │
│ │ pixel_values: (1,3,896,896)│
│ │ │
│ └── True & ratio > 1.2: │
│ "Here is the original │
│ image <boi> and here │
│ are some crops ... │
│ <boi> <boi>" │
│ boi_token 3個に増加 │
│ pixel_values: (3,3,896,896)│
│ │
│ 各 boi_token を展開: │
│ "\n\n<boi><img>×256<eoi>\n\n" │
│ │
│ tokenizer → input_ids │
│ token_type_ids 生成 │
└────────────┬────────────────┘
│
input_ids: [..., 262144×256, ...(×N)...]
pixel_values: (total_patches, 3, 896, 896)
num_patches: (num_images,)
│
═════════════╪═══════════════ CPU → GPU
│
┌────────────┴────────────────┐
│ GPU: SiglipVisionEmbeddings │
│ Conv2d(3→1152, k=14, s=14) │
│ + position_embedding │
└────────────┬────────────────┘
│
(total_patches, 4096, 1152)
│
┌────────────┴────────────────┐
│ GPU: SiglipEncoder │
│ 27層 双方向 Transformer │
│ heads=16, hidden=1152 │
└────────────┬────────────────┘
│
(total_patches, 4096, 1152)
│
┌────────────┴────────────────┐
│ GPU: Gemma3MultiModalProjector│
│ reshape → (*, 1152, 64, 64) │
│ AvgPool2d(k=4, s=4) │
│ → (*, 1152, 16, 16) │
│ flatten + transpose │
│ → (*, 256, 1152) │
│ GemmaRMSNorm(1152) │
│ matmul(1152 → 5376) │
└────────────┬────────────────┘
│
(total_patches, 256, 5376)
│
┌────────────┴────────────────┐
│ split by num_patches │
│ flatten(0, 1) per image │
│ → list[(N×256, 5376)] │
└────────────┬────────────────┘
│
┌────────────┴────────────────┐
│ GPU: embed_input_ids() │
│ │
│ text = embed_tokens(ids) │
│ × normalizer │
│ ※ 262144 は vocab 外 │
│ → handle_oov_mm_token=True │
│ → ゼロ埋め │
│ │
│ masked_scatter_( │
│ text, is_multimodal, │
│ mm_embeds) │
│ │
│ ※ vision embeds には │
│ normalizer 未適用 │
└────────────┬────────────────┘
│
(seq_len, 5376)
│
┌────────────┴────────────────┐
│ GPU: Gemma3 Decoder │
│ 62層, heads=32, kv_heads=16 │
│ sliding_window=1024 │
│ head_dim=128 │
└──────────────────────────────┘
注意事項
-
ビジョン埋め込みの正規化: テキスト埋め込みには
embed_tokens(ids) × normalizerのスケーリングが適用されるが、ビジョン埋め込みにはmm_soft_emb_norm(RMSNorm)のみが適用され、normalizerスケーリングは適用されない。 -
V1 での制限: Pan-and-Scan 有効時、V1 エンジンでは画像トークン間の双方向アテンションが簡略化されたパターンで実装されており、元モデルのアテンションパターンと完全には一致しない。
-
AvgPool2d の役割: エンコーダは 4096 パッチ(64×64 グリッド)の高解像度で処理しつつ、AvgPool2d(k=4, s=4) で 256 トークン(16×16)に圧縮して LLM に渡す。これにより計算量と情報量のバランスを取っている。
-
Pan-and-Scan のプロンプト: クロップありの場合のみ、Processor が “Here is the original image … and here are some crops to help you see better …” という装飾テキストを自動挿入する。クロップなしの場合この装飾テキストは存在せず、
<image>トークン列のみとなる。ユーザーはクロップの存在を意識する必要はない。 -
token_id=262144 の扱い:
<image>トークンの token_id=262144 は通常の vocab 範囲外(OOV)。handle_oov_mm_token=Trueにより安全にゼロ埋めされ、後続のmasked_scatter_でビジョン埋め込みに上書きされる。 -
Pan-and-Scan のデフォルト値の出典:
min_ratio_to_activate=1.2等の値は Google がモデルと共に配布した設定ではなく(preprocessor_config.json では全て null)、HF transformers のprocessing_gemma3.py内のGemma3ProcessorKwargs._defaultsにハードコードされたフォールバック値。
主要ファイル参照
| ファイル | 主要クラス/関数 |
|---|---|
| vllm/…/gemma3_mm.py | Gemma3ForConditionalGeneration, Gemma3MultiModalProjector, Gemma3ProcessingInfo |
| vllm/…/siglip.py | SiglipVisionModel, SiglipVisionEmbeddings, SiglipEncoder |
| vllm/…/utils.py | _merge_multimodal_embeddings() |
| HF transformers/…/processing_gemma3.py | Gemma3Processor, Gemma3ProcessorKwargs (デフォルト値定義) |
| HF transformers/…/image_processing_gemma3.py | Gemma3ImageProcessor (Pan-and-Scan 実装) |
LMCache 統合調査報告 [MEDIUM] [VERIFIED]
最終更新: 2026-02-15 対象ソース:
target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/lmcache_connector.py,target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/lmcache_integration/,target/LMCache/lmcache/
調査目的
LMCacheがvLLMのKVConnectorBase_V1インタフェースをどのように実装し、チャンク単位のKVキャッシュ保存・取得を実現しているかを明らかにする。
LMCache 概要
LMCacheはLLM推論のKVキャッシュを外部に保存・共有するためのライブラリ。vLLMと統合して、同じプロンプトプレフィックスのKVキャッシュを再利用したり、Prefill/Decode分離(Disaggregated Serving)でKVキャッシュを転送したりする。
チャンク単位保存
KVキャッシュをトークン列のチャンク(デフォルト256トークン)に分割して保存する。各チャンクは独立したキーで管理される。
プロンプト: [t0, t1, ..., t511]
→ チャンク0: [t0..t255] のKV → CacheEngineKey(chunk_hash=hash([t0..t255]))
→ チャンク1: [t256..t511] のKV → CacheEngineKey(chunk_hash=hash([t0..t511]))
ハッシュチェーン: チャンクハッシュはプレフィックス全体のハッシュ(先頭からそのチャンク末尾まで)。これにより、同一プレフィックスを持つ異なるリクエスト間でKVキャッシュが共有可能。
CacheEngineKey
@dataclass(slots=True)
class CacheEngineKey:
model_name: str # モデル名
world_size: int # TP並列度
worker_id: int # TPランク
chunk_hash: int # トークン列ハッシュ
dtype: torch.dtype # KVキャッシュのデータ型
request_configs: dict | None # リクエスト固有タグ
tags: tuple | None # (key, value) ペア
参照: target/LMCache/lmcache/utils.py:330-410
3層ストレージ階層
graph LR
GPU["GPU<br/>(vLLM paged buffer)"]
L1["L1: LocalCPU<br/>(ピン留めメモリ)"]
L2["L2: LocalDisk<br/>(NVMe/SSD)"]
L3["L3: Remote<br/>(Redis/S3/FS/etc.)"]
GPU <-->|"GPUConnector"| L1
L1 <-->|"StorageManager"| L2
L2 <-->|"StorageManager"| L3
| 層 | バックエンド | 設定 | 容量目安 |
|---|---|---|---|
| L1 | LocalCPUBackend | local_cpu=True, max_local_cpu_size | ~5GB |
| L2 | LocalDiskBackend | local_disk, max_local_disk_size | ~数十GB |
| L3 | RemoteBackend | remote_url | 無制限 |
追加バックエンド: P2PBackend(GPU直接), GdsBackend(GPU Direct Storage), NixlStorageBackend(RDMA), PDBackend(P/D分離用)
参照: target/LMCache/lmcache/v1/storage_backend/
リモートコネクタ(15+実装)
| コネクタ | URLスキーム | 用途 |
|---|---|---|
| RedisConnector | redis:// | Redis単体 |
| RedisSentinelConnector | redis-sentinel:// | Redis Sentinel |
| RedisClusterConnector | redis:// (cluster) | Redisクラスタ |
| S3Connector | s3:// | AWS S3 |
| FSConnector | fs:/// | ローカルファイルシステム |
| MooncakestoreConnector | mooncakestore:// | Mooncake |
| ValkeyConnector | valkey:// | Valkey |
| EICConnector | infinistore:// | InfiniStore |
参照: target/LMCache/lmcache/v1/storage_backend/connector/
vLLM統合アーキテクチャ
2つの実装パス
LMCacheConnectorV1はvLLM側のラッパーで、use_native設定により2つの実装を切り替える。
# target/vllm/.../lmcache_connector.py:83-101
use_native = vllm_config.kv_transfer_config.get_from_extra_config("use_native", False)
if use_native:
# vLLM内蔵の native 実装
cls = lmcache_integration.vllm_v1_adapter.LMCacheConnectorV1Impl
else:
# lmcache パッケージの latest 実装
cls = lmcache.integration.vllm.vllm_v1_adapter.LMCacheConnectorV1Impl
| パス | ソース | 用途 |
|---|---|---|
| native | target/vllm/.../lmcache_integration/vllm_v1_adapter.py | vLLM同梱版。安定性重視 |
| latest | target/LMCache/lmcache/integration/vllm/vllm_v1_adapter.py | LMCache最新版。機能追加優先 |
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/lmcache_connector.py:72-103
クラス階層
graph TD
KVBase["KVConnectorBase_V1<br/>(vLLM抽象基底)"]
LMC["LMCacheConnectorV1<br/>(vLLM側ラッパー)"]
Impl["LMCacheConnectorV1Impl<br/>(実装本体)"]
Engine["LMCacheEngine<br/>(チャンク管理)"]
SM["StorageManager<br/>(バックエンド管理)"]
GPU["GPUConnector<br/>(paged buffer橋渡し)"]
TDB["TokenDatabase<br/>(トークン→チャンク)"]
LC["LookupClient<br/>(Scheduler側キャッシュ問い合わせ)"]
KVBase -->|"継承"| LMC
LMC -->|"_lmcache_engine"| Impl
Impl -->|"lmcache_engine (Worker)"| Engine
Impl -->|"lookup_client (Scheduler)"| LC
Engine --> SM
Engine --> GPU
Engine --> TDB
ロール別初期化
LMCacheConnectorV1Impl.__init__()はロールにより異なるコンポーネントを初期化する。
Scheduler側 (role=SCHEDULER):
LookupClient— 外部KVキャッシュの存在確認(get_num_new_matched_tokens用)_request_trackers— リクエスト状態管理load_specs— ロード仕様管理lmcache_engine = None— エンジンは持たない
Worker側 (role=WORKER):
LMCacheEngine— KVキャッシュの保存・取得エンジンLookupServer— Scheduler側LookupClientへの応答ZMQOffloadServer— KVオフロードサーバGPUConnector— vLLMのpaged bufferとの橋渡し(3種: Paged/Layerwise/Buffer)
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/lmcache_integration/vllm_v1_adapter.py:570-715
主要データ構造
RequestTracker
リクエストのライフサイクルを追跡する。
@dataclass
class RequestTracker:
req_id: str
prompt_len: int # プロンプト全長
token_ids: list[int] # スケジュール済みトークン列
allocated_block_ids: list[int] # 割り当て済みブロックID
num_saved_tokens: int = 0 # 保存済みトークン数
disagg_spec: DisaggSpec | None # P/D分離仕様
mm_hashes: list[str] | None # マルチモーダルハッシュ
mm_positions: list[PlaceholderRange] | None # MM位置情報
request_configs: dict | None # リクエスト固有設定
is_decode_phase: bool = False # デコードフェーズか
skip_save: bool = False # 保存スキップフラグ
ライフサイクル:
from_new_request()— 新規リクエスト時にSchedulerOutputから生成update()— 各stepで新トークン・新ブロック追加is_decode_phase— 新トークン数=1でデコードフェーズ判定
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/lmcache_integration/vllm_v1_adapter.py:121-246
ReqMeta
各stepでWorkerに送信されるメタデータ。
@dataclass
class ReqMeta:
req_id: str
token_ids: list[int] # 保存/ロード対象トークン
slot_mapping: torch.Tensor # vLLM paged bufferへのマッピング
is_last_prefill: bool = False # 最終プリフィルステップか
save_spec: SaveSpec | None # セーブ仕様
load_spec: LoadSpec | None # ロード仕様
disagg_spec: DisaggSpec | None # P/D分離仕様
request_configs: dict | None # リクエスト固有設定
slot_mapping計算: block_id * block_size + offset(vLLMのBlockTable.compute_slot_mappingと同じ方式)
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/lmcache_integration/vllm_v1_adapter.py:248-398
LoadSpec / SaveSpec
@dataclass
class LoadSpec:
vllm_cached_tokens: int # vLLMローカルキャッシュ済みトークン数
lmcache_cached_tokens: int # LMCacheキャッシュ済みトークン数
can_load: bool # Schedulerがロードを許可
@dataclass
class SaveSpec:
skip_leading_tokens: int # スキップする先頭トークン数(既保存分)
can_save: bool # セーブ実行するか
セーブ判定ロジック
以下のいずれかに該当する場合、セーブをスキップ:
- 既に保存済み(
num_saved_tokens > 0)で、未保存トークンがチャンク境界に達していない - デコードフェーズで
save_decode_cache=False - リクエスト設定で
lmcache.skip_save=True - Disagg接続でない場合のみスキップ(Disaggは転送のためスキップ不可)
部分チャンク(チャンクサイズ未満)はdiscard_partial_chunks設定でセーブ可否が決まる。最終プリフィルでない場合は必ず破棄される(次stepでトークンが追加されるため)。
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/lmcache_integration/vllm_v1_adapter.py:295-338
KV形状とGPUConnector
KV形状
kv_shape = (num_layer, 1 if use_mla else 2, chunk_size, num_kv_head, head_size)
# 通常: (num_layer, 2, 256, num_kv_heads, head_size)
# 2 = Key + Value
# MLA: (num_layer, 1, 256, 1, aligned_head_size)
# 1 = compressed_kv (KeyとValueが圧縮済み)
GPUConnector 3種
| コネクタ | 用途 | 特徴 |
|---|---|---|
VLLMPagedMemGPUConnectorV2 | 標準 | vLLMのpaged bufferから直接読み書き |
VLLMPagedMemLayerwiseGPUConnector | レイヤー別 | レイヤーごとに個別処理 |
VLLMBufferLayerwiseGPUConnector | Blending用 | 中間バッファ経由でブレンディング |
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/lmcache_integration/vllm_v1_adapter.py:500-541
エンドツーエンドフロー
ロード(外部KV取得)
sequenceDiagram
participant S as Scheduler
participant LC as LMCache (SCHEDULER)
participant W as Worker
participant LW as LMCache (WORKER)
participant ST as StorageBackend
Note over S: WAITING処理
S->>LC: get_num_new_matched_tokens(request, local_computed)
LC->>LC: LookupClientでキャッシュ存在確認
LC-->>S: (external_tokens, False)
Note over S: allocate_slots()
S->>LC: update_state_after_alloc(request, blocks, external_tokens)
LC->>LC: RequestTracker作成、LoadSpec設定
Note over S: schedule()末尾
S->>LC: build_connector_meta(scheduler_output)
LC->>LC: 各RequestTrackerからReqMeta生成
LC-->>S: LMCacheConnectorMetadata
Note over S: SchedulerOutput送信
S->>W: SchedulerOutput (kv_connector_metadata含む)
Note over W: execute_model()
W->>LW: bind_connector_metadata(metadata)
W->>LW: start_load_kv(forward_context)
LW->>ST: チャンク取得(GPU→CPU→Storage)
LW->>LW: slot_mappingでpaged bufferに書き込み
Note over W: Attention層 forward
W->>LW: wait_for_layer_load(layer_name)
LW-->>W: ロード完了
Note over W: forward完了
W->>LW: get_finished(finished_req_ids)
LW-->>W: KVConnectorOutput
セーブ(KV保存)
sequenceDiagram
participant W as Worker
participant LW as LMCache (WORKER)
participant ST as StorageBackend
Note over W: Attention層 forward
W->>LW: save_kv_layer(layer_name, kv_layer, attn_metadata)
LW->>LW: ReqMetaからsave_spec確認
LW->>LW: slot_mappingでpaged bufferから読み出し
LW->>ST: チャンク保存(非同期)
Note over W: forward完了後
W->>LW: wait_for_save()
LW-->>W: 全チャンク保存完了
Note over W: リクエスト完了時
W->>LW: request_finished(request, block_ids)
LW-->>W: (delay_free=True, kv_transfer_params)
Note over W: ブロック解放は送信完了後
LMCacheEngine 内部
StorageManager
OrderedDict[name, StorageBackendInterface]でバックエンドを管理。ルックアップは登録順に検索し、最初にヒットしたバックエンドから取得する。保存は全バックエンドに書き込む(write-through)。
TokenDatabase
トークン列からチャンクキー(CacheEngineKey)へのマッピングを管理。2つの実装:
ChunkedTokenDatabase— 固定チャンクサイズで分割SegmentTokenDatabase— セグメント単位で管理
KVイベント生成
LMCacheConnectorV1はWorker側でKV保存時にBlockStoredイベントを生成する。これはvLLMのKVイベントシステム(kv_events.py)に変換され、外部のルーティングシステム等に配信される。
# LMCacheConnectorV1.get_kv_connector_kv_cache_events()
events = self._lmcache_engine.get_kv_events()
blocks = [BlockStored(block_hashes=e.block_hashes, ...) for e in events]
lmcache_kv_events = LMCacheKVEvents(num_workers=1)
lmcache_kv_events.add_events(blocks)
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/lmcache_connector.py:220-244
Disaggregated Serving(P/D分離)
LMCacheはDisaggregated Serving(PrefillインスタンスとDecodeインスタンスの分離)もサポートする。
DisaggSpec
@dataclass
class DisaggSpec:
req_id: str
receiver_id: str # 受信側エンジンID
receiver_host: str # 受信側ホスト
receiver_init_port: int # 初期化ポート
receiver_alloc_port: int # 割り当てポート
is_last_prefill: bool = False
num_transferred_tokens: int = 0
P/D分離時のフロー:
- Producerインスタンス(
kv_role=kv_producer)がプリフィル実行 - KVキャッシュをLMCache経由で保存/転送
- Consumerインスタンス(
kv_role=kv_consumer)がKVキャッシュをロード - Consumerはデコードのみ実行
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/lmcache_integration/vllm_v1_adapter.py:88-99
設定
vLLM側(KVTransferConfig)
vllm serve model_name \
--kv-transfer-config '{"kv_connector": "LMCacheConnectorV1", "kv_role": "kv_both"}'
LMCache側(LMCACHE_CONFIG_FILE)
環境変数LMCACHE_CONFIG_FILEでYAML設定ファイルを指定。主要設定:
| 設定 | デフォルト | 用途 |
|---|---|---|
chunk_size | 256 | チャンクサイズ(トークン数) |
local_cpu | True | CPUキャッシュ有効化 |
max_local_cpu_size | 5.0 | CPU最大容量(GB) |
local_disk | None | ディスクキャッシュパス |
remote_url | None | リモートストレージURL |
enable_async_loading | True | 非同期ロード |
use_layerwise | False | レイヤー別処理 |
enable_blending | False | CacheBlendモード |
save_decode_cache | False | デコードKVも保存 |
save_unfull_chunk | True | 部分チャンク保存 |
enable_pd | False | P/D分離モード |
vLLM extra_configからの設定伝搬
kv_connector_extra_configでlmcache.プレフィックス付きの設定をLMCacheに渡せる:
{
"kv_connector": "LMCacheConnectorV1",
"kv_role": "kv_both",
"kv_connector_extra_config": {
"lmcache.chunk_size": 512,
"lmcache.skip_save": true
}
}
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/lmcache_integration/vllm_v1_adapter.py:586-601
LMCacheディレクトリ構造
target/LMCache/lmcache/
├── v1/
│ ├── cache_engine.py # LMCacheEngine(中核エンジン)
│ ├── config.py # LMCacheEngineConfig
│ ├── metadata.py # LMCacheMetadata
│ ├── manager.py # LMCacheManager(ライフサイクル)
│ ├── storage_backend/
│ │ ├── storage_manager.py # StorageManager
│ │ ├── abstract_backend.py # StorageBackendInterface
│ │ ├── local_cpu_backend.py # LocalCPUBackend (L1)
│ │ ├── local_disk_backend.py # LocalDiskBackend (L2)
│ │ ├── remote_backend.py # RemoteBackend (L3)
│ │ ├── connector/ # 15+リモートコネクタ
│ │ ├── cache_policy/ # Evictionポリシー
│ │ └── naive_serde/ # シリアライゼーション
│ ├── gpu_connector/ # GPU↔CPU橋渡し
│ ├── compute/ # CacheBlend等
│ ├── token_database/ # トークン→チャンクマッピング
│ ├── lookup_client/ # Scheduler側問い合わせ
│ └── ...
├── integration/
│ └── vllm/ # vLLM統合(latest版)
│ ├── lmcache_connector_v1.py # エントリポイント
│ ├── vllm_v1_adapter.py # 主実装(~700行)
│ └── utils.py
└── utils.py # CacheEngineKey等
CacheBlend
CacheBlendはRAGシナリオでの非プレフィックスKV再利用を実現する高度な機能。enable_blending=Trueで有効化。段落(チャンク)単位でKVキャッシュを保存し、異なるコンテキストに挿入されたチャンクのKVを再利用する際に、少量の重要tokenのみを選択的に再計算する。
詳細は別ドキュメント: CacheBlend実装調査報告
要点
- vLLM本体パッチが必要:
gpu_worker.pyにモデルオブジェクト登録コード追加 - 独自forward path: vLLMのAttention層をバイパスし、LMCache内で独自にforward計算
- 対応モデル3種: Llama, Qwen2, Qwen3のみ
- 専用GPUコネクタ:
VLLMBufferLayerwiseGPUConnector(中間バッファ+RoPE位置補正+パイプライン) - 制約多数: TP/PP未対応、プレフィックスキャッシュ非互換、バッチサイズ1前提
ECConnectorとの類似点・相違点
| 観点 | LMCache (KV Transfer) | ECConnector |
|---|---|---|
| 対象 | デコーダKVキャッシュ | エンコーダキャッシュ |
| 粒度 | チャンク単位(256トークン) | エンコーダ出力全体 |
| ストレージ | 3層階層(CPU/Disk/Remote) | safetensors(参照実装) |
| 問い合わせ | LookupClient(非同期) | has_cache_item(同期) |
| 非同期ロード | あり(WAITING_FOR_REMOTE_KVS) | なし |
| P/D分離 | サポート | なし |
| KVイベント | BlockStored/Removed | なし |
| 成熟度 | 本番利用可能(10+コネクタ) | 参照実装段階 |
mm_cache 内部実装調査 [MEDIUM] [VERIFIED]
最終更新: 2026-02-18
CPU側(フロントエンドプロセス P0)でHugging FaceのPreprocessorが画像等を処理した結果をキャッシュする mm_cache の内部実装・設定・CPU並列化の調査報告。GPU側の EncoderCache(dict[str, Tensor])とは完全に別の機構。
1. mm_cacheとは
graph LR
A["API Request<br>(画像バイト列)"] --> B["HF Processor<br>(リサイズ・正規化・パッチ分割)"]
B --> C["pixel_values テンソル<br>+ prompt_updates"]
C --> D["ProcessorCache<br>★CPU側 mm_cache ここ★"]
D --> E["EngineCoreRequest<br>(ZMQ IPC)"]
E --> F["EncoderCache<br>GPU側"]
キャッシュキー: mm_hash(blake3/sha256によるコンテンツハッシュ、mm-processing.md §3 参照)
キャッシュ値: HF Processor実行結果(pixel_values 等のテンソル群 + prompt_updates)
キャッシュヒット時にスキップされる処理:
- HF Processorの実行(画像リサイズ・正規化・パッチ分割)
- テンソルデータのZMQ IPC転送(
lru/shmモード時)
2. Config設定項目
参照: target/vllm/vllm/config/multimodal.py:103(MultiModalConfig)
| フィールド名 | CLIオプション | デフォルト | 制約 | 説明 |
|---|---|---|---|---|
mm_processor_cache_gb | --mm-processor-cache-gb | 4 | ≥0 | キャッシュ容量(GiB)。0 で完全無効 |
mm_processor_cache_type | --mm-processor-cache-type | “lru” | "lru" or "shm" | キャッシュ実装タイプ |
mm_shm_cache_max_object_size_mb | (直接CLIオプションなし) | 128 | ≥0 | shmモード時の1オブジェクト上限(MiB)。shm専用 |
注意: キャッシュはプロセスごとに独立して確保される。総メモリ使用量 =
mm_processor_cache_gb × (api_server_count + data_parallel_size)。
設定変更例
# キャッシュを8GBに増量(デフォルト4GB)
vllm serve <model> --mm-processor-cache-gb 8
# 共有メモリキャッシュに切り替え(マルチworker時に効率的)
vllm serve <model> --mm-processor-cache-type shm
# キャッシュを無効化
vllm serve <model> --mm-processor-cache-gb 0
# Python APIから
from vllm import LLM
llm = LLM(model=..., mm_processor_cache_gb=8)
3. キャッシュタイプの選択ロジック
参照: target/vllm/vllm/multimodal/registry.py:281(_get_cache_type())
mm_processor_cache_gb <= 0
→ None(キャッシュ無効)
マルチモーダル非対応モデル
→ None
IPC非対応(以下のいずれか):
- api_process_count > 1(マルチAPIプロセス)
- data_parallel_size > 1 かつ data_parallel_external_lb が False
→ "processor_only"(P0のみのLRU)
IPC対応:
mm_processor_cache_type == "lru" → "lru"(SenderCache + ReceiverCache)
mm_processor_cache_type == "shm" → "shm"(共有メモリ)
processor_onlyは IPC 不可時の自動フォールバックで、ユーザーが直接指定するオプションではない。
4. キャッシュ実装5クラスの詳細
参照: target/vllm/vllm/multimodal/cache.py
4.1 P0(送信側)3実装
MultiModalProcessorOnlyCache(L326)— processor_only モード
IPC非対応時に使用。P0プロセス内でHF処理結果を丸ごとLRUに保持し、ZMQ経由で毎回テンソルを送信。
self._cache = LRUCache(
GiB_bytes * mm_config.mm_processor_cache_gb,
getsizeof=lambda x: MultiModalCache.get_item_size(x)
)
# キャッシュ値: MultiModalProcessorCacheItem(item テンソル + prompt_updates)
MultiModalProcessorSenderCache(L379)— lru モード
P0はサイズメタデータのみ保持(テンソルはP1側に保存)。IPC経路のメモリ使用を削減。
self._cache = LRUCache(
GiB_bytes * mm_config.mm_processor_cache_gb,
getsizeof=lambda x: MultiModalCache.get_item_size(x)
)
# キャッシュ値: MultiModalProcessorCacheItemMetadata(item_size + prompt_updates)
# ヒット時: data=None で ZMQ 送信(テンソル転送スキップ)
ShmObjectStoreSenderCache(L437)— shm モード
テンソルを共有メモリに書き込み、Worker は直接共有メモリから読む(ZMQ 経由のコピー不要)。
ring_buffer = SingleWriterShmRingBuffer(
data_buffer_size=int(mm_config.mm_processor_cache_gb * GiB_bytes),
name=envs.VLLM_OBJECT_STORAGE_SHM_BUFFER_NAME,
create=True, # P0がWriter
)
self._shm_cache = SingleWriterShmObjectStorage(
max_object_size=mm_config.mm_shm_cache_max_object_size_mb * MiB_bytes,
n_readers=self.world_size,
ring_buffer=ring_buffer,
serde_class=MsgpackSerde,
)
# P0-private dict に prompt_updates のみ別途保管
self._p0_cache: dict[str, Sequence[ResolvedPromptUpdate]] = {}
- エビクション: FIFO(LRUではない)
- 1オブジェクト上限を超えると
ValueError/MemoryError→ フォールバックで元の mm_input をそのまま返す
4.2 P1(受信側)2実装
MultiModalReceiverCache(L614)— lru モード時 EngineCore
P1でテンソルをLRU保持。P0のSenderCacheと同じEviction順序を維持することでキャッシュ状態を同期。
self._cache = LRUCache(
GiB_bytes * mm_config.mm_processor_cache_gb,
getsizeof=lambda x: MultiModalCache.get_item_size(x)
)
# キャッシュ値: MultiModalKwargsItem(テンソルデータそのもの)
ShmObjectStoreReceiverCache(L662)— shm モード時 Worker
共有メモリ上のアドレスからテンソルを直接読み取る。reader_lock(multiprocessing.Lock)で参照カウントを保護。
5. LRUキャッシュの内部実装
参照: target/vllm/vllm/utils/cache.py:51(vllm.utils.cache.LRUCache)
cachetools.LRUCache を継承したサイズベースのLRU実装:
| 機能 | 実装 |
|---|---|
| 容量 | maxsize = GiB_bytes * mm_processor_cache_gb(バイト数) |
| サイズ計算 | getsizeof() コールバック経由で tensor.nbytes を再帰的に合計 |
| Eviction | 最低使用時間(LRU)順。ピン済みエントリはスキップ |
| ヒット統計 | stat(delta=True) でインターバル別 hits/total を取得可能 |
| ピン機能 | pin(key) でEviction対象外にできる |
touch() | Eviction順序を更新(P0-P1同期用) |
サイズ計算の詳細(MultiModalCache.get_item_size()):
# json_map_leaves + json_reduce_leaves で再帰的にリーフ要素のサイズを合計
# leaf ごと: torch.Tensor → tensor.nbytes, str → sys.getsizeof, etc.
6. SHMキャッシュ(SingleWriterShmRingBuffer)の内部実装
参照: target/vllm/vllm/distributed/device_communicators/shm_object_storage.py:22
リングバッファ構造
バッファ全体 (mm_processor_cache_gb GiB)
┌─────────────────────────────────────────────────┐
│ [4B id][4B size][data...] [4B id][4B size][data..] │
│ ^start ↑ ^end│
└─────────────────────────────────────────────────┘
- シングルライター: P0のみが書き込み(
allocate_buf) - マルチリーダー: Worker×TP数が読み取り可能(
access_buf) - FIFOエビクション: 最も古い(start側の)オブジェクトから解放
- 参照カウント: 全リーダーが解放を確認してからGC(ライターが管理)
- ラップアラウンド: バッファ末尾に達したら先頭に折り返す
SingleWriterShmObjectStorage の付加機能
| 機能 | 実装 |
|---|---|
| キー管理 | key_index: dict[str, (address, monotonic_id)](ライター側のみ) |
| 重複キー | 既存キーはデータ再書き込みなし(アドレス再参照) |
| シリアライズ | MsgpackSerde(デフォルト) |
| 上限超過 | ValueError/MemoryError → SenderCacheがフォールバック |
| 上限チェック | put() 時に max_object_size と比較。超えたらエラー |
メモリレイアウト(オブジェクト単位):
[4-byte ref_count][metadata_size][serialized_object_data]
7. P0–P1 キャッシュ整合性の設計
参照: target/vllm/vllm/multimodal/cache.py:175(BaseMultiModalCache docstring)
P0: From API --> is_cached() × N --> get_and_update() --> To P1
P1: From P0 --> get_and_update() --> To model
核心: get_and_update() は P0 と P1 で必ず同じ順序で呼ばれる必要がある。これにより、P0のキャッシュ状態だけを参照してP1のキャッシュ状態を推定でき、IPC通信なしでキャッシュヒット確認が可能。
is_cached(): Eviction順序を変えない(P0のみ参照)get_and_update(): Eviction順序を更新(P0・P1で順番に呼ぶ)touch_sender_cache_item(): Sender側のEviction順序を手動更新(LoRA切り替え等で使用)
8. CPU並列処理
intra-request 並列化(1リクエスト内)
参照: target/vllm/vllm/v1/engine/input_processor.py:363
with set_request_id(request_id), set_default_torch_num_threads():
processed_inputs = self.input_preprocessor.preprocess(...)
set_default_torch_num_threads()(target/vllm/vllm/utils/torch_utils.py:106):
# OMP_NUM_THREADS 環境変数を読んで torch.set_num_threads() を設定
# 未設定時のデフォルト: 1
num_threads = int(os.environ.get("OMP_NUM_THREADS", 1))
torch.set_num_threads(num_threads)
HF Processorの実行(apply() → _cached_apply_hf_processor() → _call_hf_processor())はすべて同期処理。OMP_NUM_THREADS を増やすとTorchのOpenMP並列化(行列演算等)が有効になる。
OMP_NUM_THREADS=4 vllm serve <model> # 1画像あたりのCPU処理を4スレッド化
inter-request 並列化(複数リクエスト間)
vLLMには複数リクエストを同時にHF Processorで処理する機構はない。
process_inputs()は 同期呼び出し(async/await なし)- AsyncLLMの
add_request()内で呼ばれ、asyncioイベントループで逐次実行される - 並列化する唯一の手段: 複数のAPIサーバープロセスを立ち上げる
- ただし各プロセスが独立して
mm_processor_cache_gbGiBを確保する - IPC非対応になり
processor_onlyモードに自動フォールバック
- ただし各プロセスが独立して
mm_cacheによる実質的な高速化
同一画像が何度も送られる場合(例: 同じシステム画像を全リクエストで使用)、キャッシュヒット時は HF Processor の実行を完全にスキップできるため、事実上の並列化効果が得られる。
9. キャッシュモード別データフロー比較
| processor_only | lru | shm | |
|---|---|---|---|
| P0保持 | テンソル + prompt_updates | サイズメタデータ + prompt_updates | SHMアドレス + prompt_updates(P0-private dict) |
| P1保持 | なし | テンソル(LRU) | 共有メモリ上のテンソル |
| エビクション | LRU | LRU(P0/P1連動) | FIFO |
| IPCヒット時 | テンソルをZMQ送信 | data=None 送信(テンソル省略) | data=None 送信(SHMアドレスのみ) |
| MissのZMQ転送 | テンソル全体 | テンソル全体 | data=None(SHM経由) |
| 適用場面 | マルチAPIプロセス / DP≥2 | 単一APIプロセス + 単一DP(デフォルト) | 同左(マルチWorkerで転送削減) |
関連ドキュメント
- マルチモーダル処理パイプライン(§3 MMハッシュ DEEP) — ProcessorCacheのキー計算
- マルチモーダル処理パイプライン概要 — 3層キャッシュ構造図
- EncoderCache(GPU側) — GPU側の別キャッシュ機構
- Gemma3ビジョンパイプラインのキャッシュ機構 — 3層キャッシュの具体例
主要ファイル
| ファイル | 主要クラス/関数 | 行 |
|---|---|---|
target/vllm/vllm/config/multimodal.py | MultiModalConfig(mm_processor_cache_gb等) | L103 |
target/vllm/vllm/multimodal/cache.py | MultiModalProcessorOnlyCache, SenderCache, ShmObjectStoreSenderCache, ReceiverCache, ShmObjectStoreReceiverCache | L326, L379, L437, L614, L662 |
target/vllm/vllm/multimodal/registry.py | _get_cache_type(), processor_cache_from_config(), engine_receiver_cache_from_config(), worker_receiver_cache_from_config() | L281, L307, L335, L348 |
target/vllm/vllm/utils/cache.py | LRUCache(cachetools継承、サイズベース、ピン機能) | L51 |
target/vllm/vllm/distributed/device_communicators/shm_object_storage.py | SingleWriterShmRingBuffer, SingleWriterShmObjectStorage | L22, L412 |
target/vllm/vllm/v1/engine/input_processor.py | process_inputs()(set_default_torch_num_threads呼び出し) | L363 |
target/vllm/vllm/utils/torch_utils.py | set_default_torch_num_threads()(OMP_NUM_THREADS読み取り) | L106 |
プロセスアーキテクチャ(TP=2構成)
深度: [DEEP] 確信度: [VERIFIED] 最終更新: 2026-02-14
概要
vLLMをGPU2枚・TP=2で起動した場合のプロセス構成、コンポーネント配置、プロセス間通信メカニズムを調査した。
1. プロセス構成(合計4プロセス)
| プロセス名 | 生成元 | 含まれるコンポーネント |
|---|---|---|
| Frontend(メインプロセス) | ユーザー起動 | AsyncLLM, InputProcessor, EngineCoreClient, OutputProcessor |
EngineCore (EngineCore_DP0) | Frontend (mp.Process) | EngineCore, Scheduler, KVCacheManager, MultiprocExecutor |
| VllmWorker-0 | EngineCore (mp.Process) | Worker, GPUModelRunner(GPU 0) |
| VllmWorker-1 | EngineCore (mp.Process) | Worker, GPUModelRunner(GPU 1) |
参照: target/vllm/vllm/v1/engine/core_client.py:493-507 (CoreEngineProcManager)
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:147-160 (WorkerProc起動)
コンポーネントとプロセスの対応図
┌─ Frontend Process ─────────────────────────────────────┐
│ AsyncLLM ─→ InputProcessor │
│ EngineCoreClient (ZMQ ROUTER/PULL) │
│ OutputProcessor ←─ Detokenizer │
└──────────────────────────┬─────────────────────────────┘
│ ZMQ (msgpack)
▼
┌─ EngineCore Process ─────────────────────────────────────┐
│ EngineCore.step() │
│ ├─ Scheduler ─→ KVCacheManager │
│ └─ MultiprocExecutor │
│ ├─ rpc_broadcast_mq (SharedMemory → 全Worker) │
│ └─ worker_response_mq × 2 (各Worker → Executor) │
└──────────┬──────────────────────────────┬────────────────┘
│ SharedMemory MQ │ SharedMemory MQ
▼ ▼
┌─ Worker-0 Process ──┐ ┌─ Worker-1 Process ──┐
│ Worker │ │ Worker │
│ GPUModelRunner │ │ GPUModelRunner │
│ (GPU 0, TP rank 0) │ │ (GPU 1, TP rank 1) │
└──────────┬───────────┘ └──────────┬───────────┘
│ NCCL │
└─────────────────────────┘
(NVLink / PCIe 直接通信)
注意点:
- Scheduler、KVCacheManagerはEngineCoreプロセス内で動作し、独立プロセスではない
- OutputProcessorはFrontendプロセス内で動作する(バックエンドではない)
- MultiprocExecutorはEngineCoreプロセス内に存在し、Workerプロセスへの指令管理を行う
2. プロセス間通信メカニズム
2.1 Frontend ↔ EngineCore: ZMQ over TCP loopback
| 項目 | 値 |
|---|---|
| プロトコル | ZMQ over TCP (127.0.0.1:<random_port>) |
| ソケット型 | Frontend: ROUTER(送信) + PULL(受信), EngineCore: DEALER(受信) |
| シリアライゼーション | msgpack(msgspec.Struct(array_like) 対応) |
| スレッドモデル | バックグラウンドスレッドでシリアライゼーション/デシリアライゼーション |
参照: target/vllm/vllm/v1/engine/core_client.py:510-515 (ZMQソケット設定)
参照: target/vllm/vllm/v1/engine/core.py:877-950 (EngineCoreProc._perform_handshake)
2.2 EngineCore ↔ Workers: SharedMemory MessageQueue
| 項目 | 値 |
|---|---|
| プロトコル | 共有メモリ(ShmRingBuffer) + ZMQ PUB/SUB(オーバーフロー時) |
| キュー | rpc_broadcast_mq(1対多)+ worker_response_mq(各Worker→Executor) |
| シリアライゼーション | pickle(protocol 5, out-of-band buffers対応) |
| 同期方式 | ロックフリー。メモリフェンス(threading.Lock acquire/release, ~20ns)のみ |
| バッファサイズ | デフォルト24MiB/チャンク × 10チャンク |
参照: target/vllm/vllm/distributed/device_communicators/shm_broadcast.py:127 (ShmRingBuffer)
参照: target/vllm/vllm/distributed/device_communicators/shm_broadcast.py:272 (MessageQueue)
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:131-136 (rpc_broadcast_mq生成)
ShmRingBuffer メモリレイアウト
┌─────────────────────────────────┬──────────────────────────────────────┐
│ data: chunk0 | chunk1 | ... | chunkN │ metadata: [written|r0|r1|...|rN] × N │
│ max_chunks × max_chunk_bytes (24MiB) │ max_chunks × (1 + n_reader) bytes │
└─────────────────────────────────┴──────────────────────────────────────┘
メタデータの状態遷移:
0???...???: 未書き込み → 書き込み可1000...000: 書き込み直後 → 全reader読み取り可1???...???: 一部readerが読み取り済み1111...111: 全reader読み取り済み → 書き込み可(再利用)
オーバーフロー処理: データが24MiBを超える場合、ZMQ PUB/SUBソケット(IPC)経由で転送する。ローカルではXPUB/SUBソケット、リモート(マルチノード時)ではTCPソケットを使用。
MessageQueue の詳細設計 [DEEP] [VERIFIED]
MessageQueueはShmRingBufferをラップし、pickle protocol 5のout-of-bandバッファ対応のシリアライゼーション層を提供する。
ロール分離(Writer / Local Reader / Remote Reader):
| ロール | 判定条件 | 通信手段 |
|---|---|---|
| Writer | コンストラクタで生成した側 | ShmRingBuffer + ZMQ XPUB |
| Local Reader | rank in handle.local_reader_ranks | ShmRingBuffer + ZMQ SUB |
| Remote Reader | 上記以外 | ZMQ SUB のみ |
Writer側のコンストラクタでShmRingBufferとZMQソケット(XPUB)を両方作成する。Local Readerは共有メモリ経由で受信し、オーバーフロー時のみZMQ SUBにフォールバック。Remote Readerは常にZMQ SUBのみで受信する。
参照: target/vllm/vllm/distributed/device_communicators/shm_broadcast.py:272-354 (MessageQueue.init / create_from_handle)
enqueue() のデータフォーマット:
ShmRingBuffer チャンク内のバイトレイアウト:
+------+-------------------+--------------------+--------------------+-----+
| [0] | [1:3] | [3:7] [7:7+L0] | [7+L0:11+L0] ... | ... |
| flag | buf_count (2byte) | len0+main_pickle | len1+oob_buffer1 | ... |
+------+-------------------+--------------------+--------------------+-----+
flag: 0=通常, 1=オーバーフロー(ZMQ経由で後続送信)
- pickle protocol 5 + out-of-band buffers:
buffer_callbackでサイズ判定。1MiB未満のバッファはインライン化(main pickle内に含む)、1MiB以上はoob bufferとして別管理 - オーバーフロー判定:
total_bytes + len(main_pickle) >= max_chunk_bytes(デフォルト24MiB)の場合、ShmRingBufferにはflag=1のみ書き込み、実データはZMQsend_multipartで送信 - Remote Readerへは常に
send_multipartで送信(ShmRingBufferにアクセスできないため)
参照: target/vllm/vllm/distributed/device_communicators/shm_broadcast.py:571-612 (enqueue)
dequeue() のフロー:
acquire_read()でShmRingBufferからチャンクを取得- flag=0(通常): チャンクからbuf_count→各バッファ長→バッファを順次読み出し、
pickle.loads(main, buffers=oob_list)でデシリアライズ - flag=1(オーバーフロー):
acquire_read()のコンテキストを抜けてから(readフラグ設定後)、ZMQ SUBソケット経由でrecv_multipart
参照: target/vllm/vllm/distributed/device_communicators/shm_broadcast.py:614-640 (dequeue)
acquire_write() / acquire_read() の同期プロトコル:
Writer:
- メモリフェンスで最新のメタデータを読む
written_flag=0(未書き込み)または全readerが読み済み(read_count == n_reader)のチャンクを探すwritten_flagを0にリセット → データ書き込み → 全readerフラグを0にリセット → メモリフェンス →written_flagを1に → メモリフェンス- フラグ設定順序が重要: 先にreaderフラグをリセット(case 1維持)→最後にwritten=1(case 2へ遷移)。逆順だとcase 3を経由し、readerが不整合なデータを読む危険
Reader:
- メモリフェンスで最新のメタデータを読む
written_flag=1かつ自分のread_flag=0のチャンクを探す- データ読み取り → 自分の
read_flagを1に → メモリフェンス
SpinTimer / SpinSleepTimer: Readerのスピン待ち戦略。デフォルトはsched_yield()(CPU譲渡)。VLLM_SLEEP_WHEN_IDLE=1時は3秒間アクティビティがないと100msスリープに移行し、CPU消費を削減する。
参照: target/vllm/vllm/distributed/device_communicators/shm_broadcast.py:438-504 (acquire_write)
参照: target/vllm/vllm/distributed/device_communicators/shm_broadcast.py:506-569 (acquire_read)
wait_until_ready() ハンドシェイク:
Writer→各ReaderへZMQ XPUB/SUB経由でREADYメッセージを交換する集合操作。ShmRingBuffer自体にはハンドシェイクがないため、ZMQのXPUB_VERBOSE(全サブスクリプションメッセージ受信)を利用してReader接続完了を確認する。
参照: target/vllm/vllm/distributed/device_communicators/shm_broadcast.py:405-436 (wait_until_ready)
collective_rpc の動作フロー
MultiprocExecutor.collective_rpc("execute_model", args=(scheduler_output,))
│
├─ rpc_broadcast_mq.enqueue((method, args, kwargs, output_rank))
│ → pickle → ShmRingBuffer書き込み → メモリフェンス
│
├─ Worker-0: rpc_broadcast_mq.dequeue() → Worker.execute_model()
│ → worker_response_mq.enqueue((SUCCESS, output))
│
├─ Worker-1: rpc_broadcast_mq.dequeue() → Worker.execute_model()
│ → worker_response_mq.enqueue((SUCCESS, output))
│
└─ Executor: response_mqs[0].dequeue() → output[0] を返却
(output_rank=0 の場合、rank 0 の結果のみ返す)
2.4 Worker → EngineCore 結果返却パス [DEEP] [VERIFIED]
response_mq の構成
各Workerが自分専用のwriter側MessageQueue(worker_response_mq)を持ち、EngineCore側のMultiprocExecutorがそのreaderになる。rpc_broadcast_mq(1→多ブロードキャスト)とは逆方向の多→1通信だが、各MQは1 writer : 1 readerの構造。
┌─ EngineCore (MultiprocExecutor) ─────────────────────────┐
│ │
│ response_mqs[0] ◄── reader ─── worker_response_mq (W0) │
│ response_mqs[1] ◄── reader ─── worker_response_mq (W1) │
│ │
│ ※ 各MQは独立したShmRingBuffer (n_reader=1, n_local=1) │
└───────────────────────────────────────────────────────────┘
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:508-509 (Worker側: MessageQueue(1, 1))
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:172-185 (Executor側: response_mqs構築)
response_mq のハンドシェイク
- Worker側:
__init__内でMessageQueue(1, 1)を生成(writer兼ShmRingBuffer所有者) - Worker側: READYメッセージと共に
worker_response_mq.export_handle()をPipe経由でExecutor側に送信 - Executor側:
wait_for_ready()内でPipeからhandleを受信し、MessageQueue.create_from_handle(handle, 0)でreader側MQを構築 - 双方:
wait_until_ready()でZMQ XPUB/SUBハンドシェイク完了
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:757-770 (READY送信+ハンドシェイク)
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:628-646 (wait_for_response_handle_ready)
結果返却の詳細フロー
Worker.worker_busy_loop()
│
├─ rpc_broadcast_mq.dequeue() ← (method, args, kwargs, output_rank) を受信
│
├─ func = getattr(self.worker, method) ← "execute_model" 等
│
├─ output = func(*args, **kwargs) ← Worker.execute_model() 実行
│
├─ if output_rank is None or self.rank == output_rank:
│ ├─ [sync路] enqueue_output(output)
│ │ ├─ isinstance(AsyncModelRunnerOutput) → .get_output() ← GPU→CPU転送待ち
│ │ ├─ isinstance(Exception) → (FAILURE, str(e))
│ │ └─ else → (SUCCESS, output)
│ │ └─ worker_response_mq.enqueue(result)
│ │
│ └─ [async路] async_output_queue.put(output)
│ └─ async_output_busy_loop (別スレッド)
│ └─ enqueue_output(output) ← 同上
│
└─ (output_rank != self.rank の場合は何も返さない)
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:845-871 (worker_busy_loop)
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:814-843 (enqueue_output / handle_output / async_output_busy_loop)
output_rank による結果フィルタリング
collective_rpcの呼び出し時にoutput_rank(unique_reply_rankパラメータ)を指定できる:
| output_rank | Worker側の動作 | Executor側の動作 |
|---|---|---|
None | 全Workerが結果をenqueue | 全response_mqsからdequeue → リスト返却 |
0 | rank 0のみenqueue | response_mqs[0]のみdequeue → 単一値返却 |
N | rank Nのみenqueue | response_mqs[N]のみdequeue → 単一値返却 |
execute_model()はunique_reply_rank=self.output_rank(通常rank 0)で呼ばれるため、rank 0のWorkerのみが結果を返し、他のWorkerは結果を破棄する。これはTPモデルでは全Workerが同一の出力を計算するため、1つだけ返せば十分なため。
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:270-275 (execute_model → unique_reply_rank)
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:339-341 (response_mqs フィルタリング)
非同期スケジューリング(async_scheduling)
scheduler_config.async_scheduling=Trueの場合、結果返却が非同期化される:
worker_busy_loop内のhandle_output()がasync_output_queue(queue.Queue)に出力を投入- 別スレッド
async_output_busy_loop(デーモンスレッドWorkerAsyncOutputCopy)がキューから取り出し AsyncModelRunnerOutput.get_output()でGPU→CPU非同期コピー完了を待機worker_response_mq.enqueue()で結果をEngineCore側に送信
これにより、worker_busy_loopスレッドはGPU→CPUコピー完了を待たずに次のRPCを受信できる。GPU計算と結果転送をパイプライン化する仕組み。
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:560-568 (async_output_copy_thread起動)
参照: target/vllm/vllm/v1/outputs.py:200-209 (AsyncModelRunnerOutput)
non_block(FutureWrapper)
Executor側のcollective_rpc(non_block=True)では、response_mqからの結果取得を遅延評価する:
get_responseクロージャをFutureWrapperに包んで即座に返す- 次回の
collective_rpc呼び出し時に、pending futuresを先にdrainする(futures_queueから順次pop→wait_for_response) - 実際にresponse_mqから
dequeue()するのはdrain時
これにより、Executor側も結果待ちなしで次のRPCブロードキャストを発行でき、EngineCore.step()内のスケジューリングとWorkerの計算をオーバーラップできる。
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:365-375 (non_block / FutureWrapper)
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:303-375 (collective_rpc)
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:845-871 (worker_busy_loop)
2.3 Worker ↔ Worker: torch.distributed + NCCL
| 項目 | 値 |
|---|---|
| 初期化 | torch.distributed.init_process_group(backend="nccl") |
| Rendezvous | TCP(tcp://127.0.0.1:<random_port>) |
| 通信 | NCCL(NVLink / PCIe によるGPU間直接通信) |
| 用途 | Tensor Parallelのall_reduce(), all_gather(), broadcast() |
| タイミング | モデル forward pass 内部でレイヤーごとに実行 |
参照: target/vllm/vllm/v1/worker/gpu_worker.py:263-269 (init_worker_distributed_environment)
NCCLの初期化はWorker.init_device()内で、メモリプロファイリング前に行われる。これによりNCCLバッファが確保された後の利用可能メモリが正確に計測される。
3. 起動シーケンス
1. ユーザーが AsyncLLM を生成
2. AsyncLLM → EngineCoreClient.make_async_mp_client()
3. └─ mp.Process(target=EngineCoreProc.run_engine_core) 起動
4. └─ EngineCore.__init__() 内で MultiprocExecutor 生成
5. ├─ distributed_init_method = "tcp://127.0.0.1:<port>" 確保
6. ├─ rpc_broadcast_mq (ShmRingBuffer, n_reader=2) 作成
7. └─ for rank in [0, 1]:
8. mp.Process(target=WorkerProc.worker_main) 起動
9. ├─ Worker.init_device():
10. │ └─ torch.distributed.init_process_group(backend="nccl")
11. ├─ Worker.load_model(): モデルロード
12. ├─ _init_message_queues():
13. │ ├─ rpc_broadcast_mq = create_from_handle(input_shm_handle, rank)
14. │ └─ worker_response_mq = MessageQueue(1, 1) ← 各Worker独自
15. ├─ READY メッセージ + response_mq handle 送信(Pipe経由)
16. └─ wait_until_ready() → worker_busy_loop() でRPC待機開始
17. └─ wait_for_ready():
18. ├─ Pipeからhandle受信 → response_mqs[rank] 構築
19. ├─ rpc_broadcast_mq.wait_until_ready()
20. └─ 各response_mq.wait_until_ready()
21. Frontend ↔ EngineCore ZMQハンドシェイク完了
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:696 (WorkerProc.worker_main)
参照: target/vllm/vllm/v1/executor/multiproc_executor.py:752-770 (READY送信)
4. 通信方式の設計判断
なぜ Frontend ↔ EngineCore は ZMQ なのか
- 疎結合: Data Parallelism構成では別ノードに配置される可能性がある。ZMQはネットワーク透過
- asyncio統合: Frontendはasyncioイベントループ上で動作し、ZMQのasyncioポーラーと相性がよい
- バックグラウンドスレッドでの直列化: msgpackシリアライゼーションをバックグラウンドスレッドで行い、GPU計算とオーバーラップ可能
- メッセージ順序保証: ROUTER/DEALERソケットで確定的なメッセージ順序を保証
なぜ EngineCore ↔ Workers は SharedMemory MQ なのか(ZMQではない理由)
- 低レイテンシ: 同一ノード内通信に特化。ZMQはネットワークソケット抽象であり、カーネル空間でのバッファコピーとシステムコールのオーバーヘッドがある
- ゼロコピー可能: 共有メモリ上でpickleデータを直接読み書きでき、プロセス間のデータコピーが不要
- ロックフリー設計: リングバッファ + メタデータフラグ + メモリフェンス(~20ns)で同期。ロック競合なし
- collective_rpc最適化: 1対多ブロードキャスト(rpc_broadcast_mq)パターンにリングバッファが最適
なぜ Worker ↔ Worker は NCCL なのか
- GPU間テンソル通信専用: NCCLはGPUメモリ間の集合通信(all-reduce等)に特化した高性能ライブラリ
- NVLink活用: GPU間直接通信でCPU介在なし。NVLink(最大900GB/s)やPCIe(最大64GB/s)を直接利用
- PyTorch統合: モデルコード内の
torch.distributed呼び出しと直接統合 - Pythonオブジェクト不可: NCCLはテンソル転送専用であり、Pythonオブジェクト(SchedulerOutput等)の転送には使えない
通信方式比較
| 通信路 | 方式 | レイテンシ | 帯域幅 | 転送対象 | ネットワーク透過 |
|---|---|---|---|---|---|
| Frontend ↔ EngineCore | ZMQ (TCP) | ~µs | 中 | Pythonオブジェクト (msgpack) | Yes |
| EngineCore ↔ Workers | SharedMemory MQ | ~20ns同期 | 高 | Pythonオブジェクト (pickle) | No(同一ノード限定) |
| Worker ↔ Worker | NCCL | ~µs | 最高 | GPUテンソルのみ | Yes(multi-node NCCL対応) |
5. TP=1(単一GPU)との比較
TP=1の場合、UniProcExecutorが選択される:
| 項目 | TP=1 | TP=2 |
|---|---|---|
| Executor | UniProcExecutor | MultiprocExecutor |
| Workerプロセス | なし(EngineCoreプロセス内) | 2つの子プロセス |
| Worker通信 | 関数呼び出し(同一プロセス) | SharedMemory MQ |
| NCCL | 不要 | 必要(Worker間) |
| 合計プロセス数 | 2(Frontend + EngineCore) | 4(Frontend + EngineCore + Worker×2) |
参照: target/vllm/vllm/v1/executor/uniproc_executor.py:26 (UniProcExecutor)
主要ファイル
| ファイル | 主要クラス/関数 |
|---|---|
target/vllm/vllm/v1/engine/async_llm.py | AsyncLLM — Frontendプロセスのエントリポイント |
target/vllm/vllm/v1/engine/core_client.py | EngineCoreClient — ZMQ通信, CoreEngineProcManager |
target/vllm/vllm/v1/engine/core.py | EngineCore, EngineCoreProc — EngineCoreプロセスのエントリポイント |
target/vllm/vllm/v1/executor/abstract.py | Executor — collective_rpc(), execute_model() |
target/vllm/vllm/v1/executor/multiproc_executor.py | MultiprocExecutor, WorkerProc — Worker起動, MessageQueue管理, worker_busy_loop |
target/vllm/vllm/v1/executor/uniproc_executor.py | UniProcExecutor — 単一GPU用 |
target/vllm/vllm/v1/worker/gpu_worker.py | Worker — init_device(), torch.distributed初期化 |
target/vllm/vllm/distributed/device_communicators/shm_broadcast.py | ShmRingBuffer, MessageQueue — 共有メモリ通信基盤 |
Qwen3-VL-30B-A3B-Instruct: 画像推論パス全容 [MEDIUM] [VERIFIED]
対象モデル:
Qwen3-VL-30B-A3B-Instruct(model_type:qwen3_vl_moe) 調査目的: ECConnector実装に必要なエンコーダキャッシュテンソルの正確なshape/size把握 調査日: 2026-03-21 参照:target/Qwen3-VL-30B-A3B-Instruct/config.json
1. モデル構成
クラス階層
Qwen3VLMoeForConditionalGeneration (qwen3_vl_moe.py:399)
├── Qwen3_VisionTransformer (qwen3_vl.py:312) ← Vision Encoder
│ ├── Qwen3_VisionPatchEmbed (qwen3_vl.py:142)
│ ├── Qwen3_VisionBlock × 27 (qwen3_vl.py:208)
│ │ ├── Qwen2_5_VisionAttention (qwen2_5_vl.py:300)
│ │ └── Qwen3_VisionMLP (qwen3_vl.py:171)
│ ├── Qwen3_VisionPatchMerger (qwen3_vl.py:260) ← main merger
│ └── Qwen3_VisionPatchMerger × 3 (qwen3_vl.py:260) ← deepstack mergers
└── Qwen3MoeLLMForCausalLM (qwen3_vl_moe.py) ← Text (MoE)
└── Qwen3MoeLLMModel (qwen3_vl_moe.py:84)
└── Qwen3MoeSparseMoeBlock × 48 (qwen3_moe.py)
参照: target/vllm/vllm/model_executor/models/qwen3_vl_moe.py:399(登録・初期化)
コンフィグパラメータ
Vision Encoder (config.json → vision_config):
| パラメータ | 値 | 備考 |
|---|---|---|
| depth | 27 | Transformer ブロック数 |
| hidden_size | 1152 | 内部特徴量次元 |
| num_heads | 16 | head_dim = 72 |
| intermediate_size | 4304 | MLP中間層 |
| out_hidden_size | 2048 | merger出力次元(投影先) |
| patch_size | 16 | ピクセル単位(Gemma3は14) |
| spatial_merge_size | 2 | 2×2パッチを1トークンに統合 |
| temporal_patch_size | 2 | 画像は2フレームに複製して入力 |
| num_position_embeddings | 2304 | 48×48 学習済みグリッド |
| deepstack_visual_indexes | [8, 16, 24] | 中間特徴量抽出レイヤー |
| in_channels | 3 | RGB |
| hidden_act | gelu_pytorch_tanh |
Text Model (config.json → text_config):
| パラメータ | 値 | 備考 |
|---|---|---|
| num_hidden_layers | 48 | |
| hidden_size | 2048 | |
| num_attention_heads | 32 | |
| num_key_value_heads | 4 | GQA (8:1) |
| head_dim | 128 | |
| num_experts | 128 | |
| num_experts_per_tok | 8 | アクティブエキスパート数 |
| moe_intermediate_size | 768 | エキスパートあたり |
| intermediate_size | 6144 | Dense層用 |
| max_position_embeddings | 262144 | |
| rope_theta | 5,000,000 | |
| mrope_section | [24, 20, 20] | 3D M-RoPE |
| vocab_size | 151936 |
特殊トークン:
| トークン | ID | テキスト表現 |
|---|---|---|
| vision_start | 151652 | <|vision_start|> |
| vision_end | 151653 | <|vision_end|> |
| image_token | 151655 | <|image_pad|> |
| video_token | 151656 | <|video_pad|> |
2. OpenAI API → 内部表現への変換パス
graph TD
A["OpenAI ChatCompletion API<br/>(image_url in message)"] --> B["OpenAIServingChat<br/>serving_chat.py"]
B --> C["parse_chat_inputs_to_harmony_messages<br/>chat_utils.py"]
C --> D["MediaConnector.fetch_image<br/>URL/base64 → PILイメージ"]
D --> E["AsyncLLM.add_request"]
E --> F["InputProcessor<br/>(tokenize + Qwen3VLMultiModalProcessor)"]
F --> G["Qwen3VLMultiModalProcessor.apply<br/>(qwen3_vl.py:920)"]
G --> H["HF Qwen3VLProcessor.__call__<br/>(smart_resize + normalize + tokenize)"]
H --> I["EngineCoreRequest<br/>(token_ids + mm_kwargs)"]
処理順序
- OpenAI API受信:
image_url付きChatCompletionRequest - 画像取得:
MediaConnector.fetch_image()でURL/base64からPILイメージをデコード - チャットテンプレート適用: Jinja2テンプレートで
<|vision_start|><|image_pad|><|vision_end|>を挿入 - HF Processor呼び出:
Qwen3VLProcessor→Qwen2VLImageProcessorFastでリサイズ・正規化 - プレースホルダー展開:
<|image_pad|>をnum_vision_tokens個の token_id=151655 に置換 - EngineCoreRequest構築:
token_ids+mm_kwargs(pixel_values, image_grid_thw等)
3. Chat Template & Placeholder展開
テンプレート構造
参照: target/Qwen3-VL-30B-A3B-Instruct/chat_template.json
画像を含むユーザーメッセージ:
<|im_start|>user
<|vision_start|><|image_pad|><|vision_end|>この画像について説明してください<|im_end|>
<|im_start|>assistant
Placeholder展開ロジック
参照: target/vllm/vllm/model_executor/models/qwen3_vl.py:1024-1030
def get_image_replacement_qwen3vl(item_idx: int):
grid_thw = out_mm_kwargs["image"][item_idx]["image_grid_thw"].data
merge_length = merge_size ** 2 # = 4
num_tokens = int(grid_thw.prod()) // merge_length
return [hf_processor.image_token_id] * num_tokens # token_id=151655 × N
1枚の画像に対して <|image_pad|> が num_vision_tokens 個のトークン(全て token_id=151655)に展開される。
4. 画像前処理(Preprocessing)
Processor構成
参照: target/Qwen3-VL-30B-A3B-Instruct/preprocessor_config.json
| パラメータ | 値 |
|---|---|
| processor_class | Qwen3VLProcessor |
| image_processor_type | Qwen2VLImageProcessorFast |
| patch_size | 16 |
| merge_size | 2 |
| min_pixels (shortest_edge) | 65,536 (≈256²) |
| max_pixels (longest_edge) | 16,777,216 (=4096²) |
| image_mean | [0.5, 0.5, 0.5] |
| image_std | [0.5, 0.5, 0.5] |
smart_resize
参照: target/vllm/vllm/model_executor/models/qwen3_vl.py:670-678
factor = patch_size * merge_size # = 16 × 2 = 32
resized_height, resized_width = smart_resize(
height=image_height, width=image_width,
factor=32,
min_pixels=65536,
max_pixels=16777216,
)
- 画像の H, W を 32の倍数 にリサイズ
- 総ピクセル数が
min_pixels〜max_pixelsの範囲に収まるようアスペクト比を維持してスケール smart_resizeはtransformers.models.qwen2_vl.image_processing_qwen2_vlからインポート
Vision Token数の計算式
参照: target/vllm/vllm/model_executor/models/qwen3_vl.py:682-689
# 画像の場合(num_frames=2, temporal_patch_size=2)
padded_num_frames = round_up(2, 2) # = 2
grid_t = max(2 // 2, 1) # = 1
grid_h = resized_height // 16 # patch_size
grid_w = resized_width // 16 # patch_size
num_patches = grid_t × grid_h × grid_w # = (H'/16) × (W'/16)
num_vision_tokens = num_patches // 4 # merge_size² = 4
導出: num_vision_tokens = (H'/32) × (W'/32)
5. Vision Encoder アーキテクチャ詳細
5.1 PatchEmbed
参照: target/vllm/vllm/model_executor/models/qwen3_vl.py:142-168
Conv3dLayer(
in_channels=3,
out_channels=1152,
kernel_size=(2, 16, 16), # (temporal_patch_size, patch_size, patch_size)
stride=(2, 16, 16),
bias=True,
)
- 入力:
(num_patches, 1536)— 各パッチは3ch × 2frames × 16px × 16pxがflattened - 処理:
view(L, 3, 2, 16, 16)→ Conv3d →view(L, 1152) - 出力:
(num_patches, 1152)
5.2 Position Embedding
参照: target/vllm/vllm/model_executor/models/qwen3_vl.py:464-522
- 48×48 (=2304) の学習済み位置埋め込み (
nn.Embedding(2304, 1152)) - 双線形補間 (
fast_pos_embed_interpolate): 任意のgrid_h × grid_wに対して48×48グリッドから補間 - spatial_merge_size=2 による並べ替え(2×2ブロック単位でグループ化)
- 時間次元t > 1の場合はexpand/repeat
5.3 Rotary Position Embedding
参照: target/vllm/vllm/model_executor/models/qwen3_vl.py:419-462
- 2D RoPE:
(h_pos, w_pos)座標ペアから計算 partial_rotary_factor = 0.5(head_dim 72のうち36次元にのみ適用)- max_position=8192
- テキスト側は3D M-RoPE(
mrope_section=[24, 20, 20])で別管理
5.4 Transformer Blocks (27層)
参照: target/vllm/vllm/model_executor/models/qwen3_vl.py:208-258
各ブロック:
- LayerNorm (eps=1e-6)
- Multi-Head Attention (16 heads, head_dim=72)
- Residual connection
- LayerNorm
- MLP: Linear(1152→4304) → SiLU → Linear(4304→1152)
- Residual connection
形状は全27層で不変: (num_patches, 1, 1152)(unsqueeze(1) 済み)
5.5 Deepstack Feature Extraction
参照: target/vllm/vllm/model_executor/models/qwen3_vl.py:565-579
for layer_num, blk in enumerate(self.blocks):
hidden_states = blk(hidden_states, ...)
if layer_num in self.deepstack_visual_indexes: # [8, 16, 24]
deepstack_feature = self.deepstack_merger_list[idx](hidden_states)
deepstack_feature_lists.append(deepstack_feature)
- Layer 8, 16, 24 の出力を それぞれ独立のmerger で投影
- 各deepstack merger:
use_postshuffle_norm=Truenorm(x.view(-1, 4608))→ Linear(4608→4608) → GELU → Linear(4608→2048)
- 出力:
(num_vision_tokens, 2048)× 3本
5.6 Main Merger (最終投影)
参照: target/vllm/vllm/model_executor/models/qwen3_vl.py:300-309
# use_postshuffle_norm=False (main merger)
x = self.norm(x).view(-1, self.hidden_size) # (num_patches, 1, 1152) → (num_vision_tokens, 4608)
x = self.linear_fc1(x) # Linear(4608 → 4608)
x = self.act_fn(x) # GELU
out = self.linear_fc2(x) # Linear(4608 → 2048)
# 出力: (num_vision_tokens, 2048)
hidden_size = context_dim × spatial_merge_size² = 1152 × 4 = 4608 — 空間的に隣接する2×2パッチを結合してから投影。
5.7 最終出力(Deepstack連結)
参照: target/vllm/vllm/model_executor/models/qwen3_vl.py:580-584
hidden_states = self.merger(hidden_states) # (num_vision_tokens, 2048)
hidden_states = torch.cat(
[hidden_states] + deepstack_feature_lists, dim=1
) # (num_vision_tokens, 2048 + 2048 + 2048 + 2048) = (num_vision_tokens, 8192)
★ Vision Encoder最終出力: (num_vision_tokens, 8192)
out_hidden_size の定義もこれを反映:
# qwen3_vl.py:337
self.out_hidden_size = vision_config.out_hidden_size * (1 + len(self.deepstack_visual_indexes))
# = 2048 * (1 + 3) = 8192
6. Tensor Shape遷移の全体図
入力画像 (H, W, 3)
│
↓ smart_resize: H' = round(H, 32), W' = round(W, 32)
↓ HF Processor: normalize, 2フレーム複製, パッチ分割
│
pixel_values: (num_patches, 1536)
│ num_patches = (H'/16) × (W'/16)
│ 1536 = 3 × 2 × 16 × 16
│
↓ PatchEmbed (Conv3d)
│
(num_patches, 1152)
│
↓ + position_embedding (bilinear interpolation from 48×48)
↓ unsqueeze(1)
│
(num_patches, 1, 1152)
│
├─ Layer 0〜7: VisionBlock ×8 → (num_patches, 1, 1152)
├─ Layer 8: deepstack_merger[0] → ds_8: (num_vision_tokens, 2048)
├─ Layer 9〜15: VisionBlock ×7 → (num_patches, 1, 1152)
├─ Layer 16: deepstack_merger[1] → ds_16: (num_vision_tokens, 2048)
├─ Layer 17〜23: VisionBlock ×7 → (num_patches, 1, 1152)
├─ Layer 24: deepstack_merger[2] → ds_24: (num_vision_tokens, 2048)
├─ Layer 25〜26: VisionBlock ×2 → (num_patches, 1, 1152)
│
↓ main merger: view→(num_vision_tokens, 4608)→Linear→GELU→Linear
│
main: (num_vision_tokens, 2048)
│
↓ torch.cat([main, ds_8, ds_16, ds_24], dim=1)
│
★ ENCODER OUTPUT: (num_vision_tokens, 8192) ← encoder_cache に格納
│ num_vision_tokens = num_patches // 4 = (H'/32) × (W'/32)
│
↓ _process_image_input (qwen3_vl.py:1418-1438)
↓ split by image (grid_thw.prod(-1) // merge_size // merge_size)
│
Per-image tensor: (num_vision_tokens_i, 8192) ← encoder_cache[mm_hash]
7. Encoder Cache テンソル仕様(ECConnector用)
テンソル形式
| 項目 | 値 |
|---|---|
| ndim | 2 (sanity check: target/vllm/vllm/v1/worker/utils.py:62-89) |
| shape | (num_vision_tokens, 8192) |
| dtype | bfloat16 |
| device | CUDA GPU(計算時)→ CPU(ECConnector保存時) |
| key | mm_hash (SHA256) |
| hidden_dim内訳 | 2048 (main) + 2048 (ds_layer8) + 2048 (ds_layer16) + 2048 (ds_layer24) |
画像サイズ別テーブル
| 入力画像 | resize後 | grid (t,h,w) | num_patches | num_vision_tokens | tensor shape | bfloat16 サイズ |
|---|---|---|---|---|---|---|
| 256×256 | 256×256 | (1,16,16) | 256 | 64 | (64, 8192) | 1.0 MB |
| 512×384 | 512×384 | (1,32,24) | 768 | 192 | (192, 8192) | 3.1 MB |
| 512×512 | 512×512 | (1,32,32) | 1024 | 256 | (256, 8192) | 4.2 MB |
| 768×768 | 768×768 | (1,48,48) | 2304 | 576 | (576, 8192) | 9.4 MB |
| 1024×768 | 1024×768 | (1,64,48) | 3072 | 768 | (768, 8192) | 12.6 MB |
| 1024×1024 | 1024×1024 | (1,64,64) | 4096 | 1024 | (1024, 8192) | 16.8 MB |
| 1920×1080 | 1920×1088 | (1,120,68) | 8160 | 2040 | (2040, 8192) | 33.4 MB |
| 2048×2048 | 2048×2048 | (1,128,128) | 16384 | 4096 | (4096, 8192) | 67.1 MB |
| 4096×3072 | 4096×3072 | (1,256,192) | 49152 | 12288 | (12288, 8192) | 201.3 MB |
計算式: size_bytes = num_vision_tokens × 8192 × 2
ECConnector save/loadの呼び出し箇所
Save:
# target/vllm/vllm/v1/worker/gpu_model_runner.py:2442-2445
for mm_hash, output in zip(mm_hashes, encoder_outputs):
self.encoder_cache[mm_hash] = output # (N, 8192) tensor
self.maybe_save_ec_to_connector(self.encoder_cache, mm_hash)
ECConnectorBase.save_caches() シグネチャ:
# target/vllm/vllm/distributed/ec_transfer/ec_connector/base.py:150-165
def save_caches(
self, encoder_cache: dict[str, torch.Tensor], mm_hash: str, **kwargs
) -> None:
Load:
# ECConnectorBase.start_load_caches() — base.py:132-147
def start_load_caches(
self, encoder_cache: dict[str, torch.Tensor], **kwargs
) -> None:
ECExampleConnector の保存例
参照: target/vllm/vllm/distributed/ec_transfer/ec_connector/example_connector.py
# save: GPU → CPU → safetensors
ec_cache = encoder_cache[mm_hash] # (N, 8192), bfloat16, CUDA
tensors = {"ec_cache": ec_cache.detach().cpu()} # → CPU
safetensors.torch.save_file(tensors, filename)
# load: safetensors → GPU
ec_cache = safetensors.torch.load_file(filename, device="cuda")["ec_cache"]
encoder_cache[mm_hash] = ec_cache # (N, 8192), bfloat16, CUDA
8. Deepstackの言語モデルへの注入方式
embed_input_ids での分割
参照: target/vllm/vllm/model_executor/models/qwen3_vl.py:1930-1969
# encoder_cacheから取得した (N, 8192) テンソルを分割
multimodal_embeddings_main, multimodal_embeddings_multiscale = torch.split(
multimodal_embeddings_cat,
[self.visual_dim, self.multiscale_dim], # [2048, 6144]
dim=-1,
)
# main: (N, 2048) → テキスト埋め込みのplaceholder位置にmerge
# multiscale: (N, 6144) → reshape → (3, seq_len, 2048)
テキスト埋め込みへのマージ
- main embeddings
(N, 2048):_merge_multimodal_embeddings()でplaceholderトークン位置に配置 - multiscale embeddings: reshape →
(3, total_seq_len, 2048)→_set_deepstack_input_embeds()でバッファに書き込み
MoEテキストモデルへの注入
参照: target/vllm/vllm/model_executor/models/qwen3_vl_moe.py:117-123
# Qwen3MoeLLMModel.forward()
for layer_idx, layer in enumerate(self.layers):
hidden_states, residual = layer(positions, hidden_states, residual)
if deepstack_input_embeds is not None and layer_idx in range(0, len(deepstack_input_embeds)):
hidden_states = hidden_states + deepstack_input_embeds[f"deepstack_input_embeds_{layer_idx}"]
- Layer 0:
hidden_states += deepstack_input_embeds_0(Layer 8由来) - Layer 1:
hidden_states += deepstack_input_embeds_1(Layer 16由来) - Layer 2:
hidden_states += deepstack_input_embeds_2(Layer 24由来)
deepstack は early layers に中間表現を直接加算する形で多スケール情報を注入する。
9. MoE テキストモデルの構造
Mixture of Experts 仕様
| パラメータ | 値 |
|---|---|
| 総エキスパート数 | 128 |
| アクティブエキスパート数/トークン | 8 |
| エキスパート中間層サイズ | 768 |
| Gate | ReplicatedLinear(2048, 128) |
| norm_topk_prob | true |
実効計算量: 各トークンで8エキスパート × 768中間 = 6144次元相当(Dense 6144と同等の計算)
パラメータ効率
- 全パラメータ: ~30B(128エキスパートの重み含む)
- アクティブパラメータ: ~3B(8/128 = 6.25%のエキスパートのみ活性化)
decoder_sparse_step = 1→ 全層がMoE(Dense層なし)
10. Gemma3 27B との比較
| 項目 | Gemma3-27B-IT | Qwen3-VL-30B-A3B |
|---|---|---|
| Vision Encoder | ||
| アーキテクチャ | SiglipVisionModel | Qwen3_VisionTransformer |
| patch_size | 14 | 16 |
| hidden_size | 1152 | 1152(同一) |
| depth | 27 | 27(同一) |
| 位置埋め込み | 2D learned (固定) | 2D learned + bilinear interpolation |
| RoPE | なし | 2D Partial RoPE (factor=0.5) |
| 投影 | ||
| 方式 | AvgPool2d(4) + Linear | Spatial Merge (2×2) + MLP |
| 出力次元 | 5376 (text hidden) | 2048 |
| Deepstack | なし | あり (layers 8,16,24) |
| Encoder出力dim | 5376 | 8192 (2048×4) |
| トークン数/画像 | 固定256 | 可変 (64〜12288+) |
| Temporal | なし | temporal_patch_size=2 |
| テキストモデル | ||
| アーキテクチャ | Dense Transformer | MoE (128 experts) |
| hidden_size | 5376 | 2048 |
| 層数 | 62 | 48 |
| 前処理 | ||
| リサイズ | 固定896×896 | smart_resize (32の倍数, 可変) |
| Pan-and-Scan | あり(オプション) | なし |
| 正規化 | ImageNet mean/std | mean=0.5, std=0.5 |
| キャッシュ | ||
| encoder_cache tensor | (256, 5376) 固定 | (N, 8192) 可変 |
| キャッシュサイズ/画像 | 2.6 MB 固定 | 1.0〜201+ MB 可変 |
ECConnector実装への影響
- 可変サイズテンソル: Gemma3は固定256トークンだが、Qwen3-VLは画像解像度に依存して大幅に変動(64〜12288+トークン)。ストレージ割り当てに注意が必要
- 大きなhidden_dim: 8192次元はGemma3の5376より52%大きい。deepstack情報を含むため圧縮不可
- メモリ使用量: 高解像度画像で100MB超のテンソルがありうる。ネットワーク転送コストに注意
- deepstack分割の透明性: ECConnectorは
(N, 8192)テンソルをそのまま保存/復元すればよい。分割はembed_input_ids内で行われるため、ECConnector側でのdim分割は不要
付録A: 数値計算例
例1: 1024×1024 画像
H=1024, W=1024
smart_resize → 1024×1024 (変更なし, 32の倍数)
grid_t=1, grid_h=64, grid_w=64
num_patches = 4096
num_vision_tokens = 1024
pixel_values: (4096, 1536)
PatchEmbed後: (4096, 1152)
VisionBlock後: (4096, 1, 1152)
main merger後: (1024, 2048)
deepstack × 3: (1024, 2048) × 3
最終出力: (1024, 8192)
encoder_cache: 1024 × 8192 × 2 = 16,777,216 bytes ≈ 16.8 MB
例2: 1920×1080 画像 (Full HD)
H=1080, W=1920
smart_resize → 1088×1920 (H:1080→1088, 32の倍数に切り上げ)
grid_t=1, grid_h=68, grid_w=120
num_patches = 8160
num_vision_tokens = 2040
pixel_values: (8160, 1536)
最終出力: (2040, 8192)
encoder_cache: 2040 × 8192 × 2 = 33,423,360 bytes ≈ 33.4 MB
例3: 256×256 画像 (最小クラス)
H=256, W=256
smart_resize → 256×256 (min_pixels=65536, 256²=65536 ちょうどOK)
grid_t=1, grid_h=16, grid_w=16
num_patches = 256
num_vision_tokens = 64
pixel_values: (256, 1536)
最終出力: (64, 8192)
encoder_cache: 64 × 8192 × 2 = 1,048,576 bytes ≈ 1.0 MB
ZMQ 通信パターンと信頼性分析
深度: [MEDIUM] 確信度: [VERIFIED] 日付: 2026-02-18 きっかけ: vLLM全体のプロセス間通信基盤であるZMQの使用パターンを体系的に理解し、メッセージ喪失時の挙動を分析する
問い
- vLLMはZMQのどのソケットタイプ・通信パターンを使っているか?
- ZMQにはネイティブな到達保証やリトライがないが、メッセージが喪失した場合はどうなるか?
- vLLM側で信頼性を担保する仕組みはあるか?
ZMQ使用箇所の全体像
vLLM(v1)では16ファイルでZMQが使用されており、以下の5カテゴリに分類できる。
カテゴリ一覧
| カテゴリ | ファイル数 | トランスポート | 用途 |
|---|---|---|---|
| Frontend↔EngineCore通信 | 5 | IPC / TCP | コアのリクエスト/レスポンス通信 |
| DP Coordinator | 1 | IPC / TCP | Data Parallel負荷分散・Wave調整 |
| MessageQueue (ShmRingBuffer) | 1 | IPC | SharedMemoryオーバーフロー時のフォールバック |
| KV Cache Events | 1 | TCP / IPC | 外部サービスへのKVイベント配信 |
| KV Transfer コネクタ | 8 | TCP | ノード間KVキャッシュ転送の制御チャネル |
1. Frontend↔EngineCore通信 [VERIFIED]
最も重要な通信パス。フロントエンド(AsyncLLM/LLM)とEngineCore間のリクエスト送信・レスポンス受信を担う。
ソケット構成
Frontend (MPClient) EngineCore (EngineCoreProc)
┌─────────────────┐ ┌─────────────────┐
│ input_socket │ ──ROUTER──► │ input_socket │
│ (zmq.ROUTER, │ │ (zmq.DEALER, │
│ bind=True) │ │ bind=False) │
│ │ │ │
│ output_socket │ ◄──PULL──── │ output_socket │
│ (zmq.PULL, │ │ (zmq.PUSH, │
│ bind=False) │ │ bind=True) │ ← linger=4000ms
└─────────────────┘ └─────────────────┘
参照: target/vllm/vllm/v1/engine/core_client.py:510-514 (ソケット作成)
参照: target/vllm/vllm/v1/engine/core.py:1199-1206 (EngineCore側input)
参照: target/vllm/vllm/v1/engine/core.py:1286-1296 (EngineCore側output)
パターン: ROUTER/DEALER
-
リクエスト送信: Frontend(ROUTER) → EngineCore(DEALER)
- ROUTERはidentityベースのルーティングを行う。DPモードでは複数EngineCoreへの振り分けに使用
- EngineCoreのidentityはDP rankの2バイトリトルエンディアン表現
- メッセージ形式:
(identity, request_type, serialized_data, [oob_buffers...])
-
レスポンス返却: EngineCore(PUSH) → Frontend(PULL)
- PUSH/PULLはunidirectionalで、identityルーティングなし
- 複数API serverがある場合、各API serverに別々のPUSH→PULLペア
client_indexで宛先のPUSHソケットを選択
HWM(High Water Mark)設定
参照: target/vllm/vllm/utils/network_utils.py:260-313 (make_zmq_socket)
# PULL, DEALER, ROUTER
socket.setsockopt(zmq.RCVHWM, 0) # 受信HWM無制限
socket.setsockopt(zmq.RCVBUF, buf_size) # 0.5GB or system default
# PUSH, DEALER, ROUTER
socket.setsockopt(zmq.SNDHWM, 0) # 送信HWM無制限
socket.setsockopt(zmq.SNDBUF, buf_size) # 0.5GB or system default
重要: HWMが0(無制限)に設定されているため、送信側でのメッセージドロップは発生しない。ZMQはHWMに達した場合にメッセージをドロップするが、HWM=0ではカーネルバッファが許す限りキューイングされる。
ハンドシェイクプロトコル
起動時の3段階ハンドシェイク:
- EngineCore→Frontend: DEALER→ROUTERで空メッセージ
b""送信(ROUTER側がidentityを認識するため必須) - EngineCore→Frontend:
"HELLO"メッセージ送信(DP rank、local/remote情報) - Frontend→EngineCore:
EngineHandshakeMetadata返却(ZMQアドレス、parallel_config) - EngineCore→Frontend:
"READY"メッセージ送信(初期化完了、num_gpu_blocks報告)
参照: target/vllm/vllm/v1/engine/utils.py:937-1091 (wait_for_engine_startup)
参照: target/vllm/vllm/v1/engine/core.py:870-920 (EngineCore側ハンドシェイク)
ゼロコピー送信とMessageTracker
メッセージにテンソルのバッキングバッファが含まれる場合、send_multipart(copy=False, track=True)でゼロコピー送信を行う。zmq.MessageTrackerでZMQがバッファを使い終わるまで参照を保持する。
参照: target/vllm/vllm/v1/engine/core_client.py:581-587 (pending_messages管理)
参照: target/vllm/vllm/v1/engine/core.py:1322-1332 (output側のpending管理+バッファ再利用)
2. DP Coordinator通信 [VERIFIED]
Data Parallel環境での負荷分散統計の集約・配信とWave調整を担う。
ソケット構成
Frontend(s) DPCoordinator EngineCore(s)
┌──────────┐ ┌──────────────┐ ┌──────────┐
│stats_upd │◄──XSUB────── │publish_front │ │ │
│(zmq.XSUB)│ │(zmq.XPUB, │ │ │
│ │──────────────►│ bind=True) │ │ │
│ │ 新リクエスト通知│ │ │ │
│ │ │ │ │ │
│ │ │output_back │◄──PUSH── │coord_out │
│ │ │(zmq.PULL, │ │(zmq.PUSH)│
│ │ │ bind=True) │ │ │
│ │ │ │ │ │
│ │ │publish_back │──XPUB──► │coord_in │
│ │ │(zmq.XPUB, │ │(zmq.XSUB)│
│ │ │ bind=True) │ │ │
└──────────┘ └──────────────┘ └──────────┘
参照: target/vllm/vllm/v1/engine/coordinator.py:113-395
3つの通信チャネル
- publish_front (XPUB): Coordinator→Frontend。統計情報(各エンジンのwaiting/running数)とwave状態を配信
- output_back (PULL): EngineCore→Coordinator。各エンジンのScheduler統計とwave完了通知
- publish_back (XPUB): Coordinator→EngineCore。wave開始指示のブロードキャスト
XPUB/XSUBパターン
通常のPUB/SUBと異なり、XPUB/XSUBはサブスクリプションメッセージを可視化できる:
- XPUB: subscription/unsubscriptionメッセージを受信可能 → 全サブスクライバの接続確認に使用
- XSUB: 明示的にsubscriptionメッセージを送信可能 → 動的なsubscribe制御に使用
3. MessageQueue (ShmRingBuffer) ZMQフォールバック [VERIFIED]
EngineCore↔Worker間のSharedMemory通信で、メッセージがShmRingBufferの最大チャンクサイズ(デフォルト24MiB)を超えた場合にZMQ PUB/SUBへフォールバックする。
参照: target/vllm/vllm/distributed/device_communicators/shm_broadcast.py:590-594
オーバーフロー判定
if total_bytes + len(all_buffers[0]) >= self.buffer.max_chunk_bytes:
with self.acquire_write(timeout) as buf:
buf[0] = 1 # overflow flag
self.local_socket.send_multipart(all_buffers, copy=False) # ZMQ XPUB
- ShmRingBufferのメタデータブロックに
overflow=1フラグを書き込み - 実データはXPUB→SUBソケット経由で送信
- Reader側はメタデータのoverflowフラグを確認し、ZMQソケットから読み取る
ソケット構成
- Writer:
XPUB(bind、IPC)。ローカルリーダー向け + リモートリーダー向けの2つ - Local Reader:
SUB(connect、IPC)。ShmRingBufferとペア - Remote Reader:
SUB(connect、TCP)。ShmRingBufferなし、常にZMQ経由
接続確認
wait_until_ready()で全リーダーのサブスクリプション受信→b"READY"送信で双方向の接続を確認後に通信開始。
4. KV Cache Events [VERIFIED]
KVキャッシュの変更イベント(BlockStored/BlockRemoved/AllBlocksCleared)を外部サービスに配信する。
参照: target/vllm/vllm/distributed/kv_events.py:270-400
ソケット構成
- PUB (bind、TCP
tcp://*:5557): イベントストリームの配信 - ROUTER (bind): リプレイリクエストの受け付け(過去イベントの再送要求用)
特徴
- HWM設定あり:
set_hwm(100_000)— PUBソケットにHWMが設定されている。サブスクライバが遅い場合、HWMを超えたメッセージはドロップされる - シーケンス番号: 各イベントバッチにシーケンス番号を付与
- リプレイ機能: ROUTERソケットでリプレイリクエストを受け付け、バッファリングされた過去イベントを再送可能(deque、maxlen=10,000ステップ)
- バックグラウンドスレッド: パブリッシャーは専用スレッドで動作
5. KV Transfer コネクタ [VERIFIED]
ノード間KVキャッシュ転送の制御プレーンにZMQを使用。データプレーンは各コネクタ固有(RDMA、NCCL等)。
共通パターン: ROUTER/DEALER
全コネクタで共通して ROUTER(サーバー側、bind)/ DEALER(クライアント側、connect)パターンを使用。
| コネクタ | ZMQ用途 | ソケットタイプ |
|---|---|---|
| NIXL | メタデータハンドシェイク | ROUTER/REQ |
| P2P NCCL | 転送要求・応答 | ROUTER/DEALER |
| Mooncake | サイドチャネル通知 | ROUTER/DEALER |
| MoRIIO | メタデータ交換・通知 | ROUTER/DEALER |
| LMCache MP | LookupClient/Server通信 | ZMQ経由(LMCache内部) |
NIXL特有: REQ/REP風ハンドシェイク
NIXLはROUTER/REQパターンを使用し、RDMAメモリ登録のためのメタデータ交換を行う。RCVTIMEO=5000msでタイムアウトを設定。
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/nixl_connector.py:615-618 (ROUTER側)
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/nixl_connector.py:1062-1076 (REQ側)
信頼性分析
ZMQの特性(前提知識)
ZMQはメッセージ到達保証を提供しないメッセージングライブラリ:
- TCPの上に構築されているが、接続のライフサイクル管理は自動(再接続含む)
- PUB/SUB: サブスクライバが遅い場合、HWMを超えたメッセージはサイレントにドロップされる
- PUSH/PULL: HWMに達するとブロック(またはEAGAIN)
- ROUTER/DEALER: HWMに達するとROUTER側はメッセージをドロップ(DEALERはブロック)
- IPC(同一ホスト内)はTCPより信頼性が高い(ネットワーク障害なし)
各通信パスの信頼性評価
1. Frontend↔EngineCore(最重要パス)
リスク: 低
理由:
- HWM=0(無制限): 送信側でのメッセージドロップは発生しない
- 同一ホスト内IPC: ネットワーク障害のリスクなし(DP TCPモード除く)
- プロセス死活監視:
MPClientEngineMonitorスレッドがEngineCoreプロセスのsentinelを監視。プロセス死亡→engine_dead=True→以降の操作はEngineDeadError - ENGINE_CORE_DEAD通知: EngineCore異常終了時にFrontendへ通知。
linger=4000msで送信完了を待つ - validate_alive(): 受信メッセージがENGINE_CORE_DEADか毎回チェック
メッセージ喪失時の影響:
- リクエスト喪失(Frontend→EngineCore): リクエストが処理されず、クライアントがタイムアウトするまで待機。vLLM内部でのリトライ機構はない
- レスポンス喪失(EngineCore→Frontend): リクエスト結果が返却されず、クライアントがタイムアウト。Schedulerのリクエストは残り続け、abort時に解放
実際にメッセージ喪失が起きうるか: IPCかつHWM=0の環境では、プロセスが正常動作している限りメッセージ喪失は極めて起きにくい。主なリスクはプロセスクラッシュ。
2. DP Coordinator通信
リスク: 低〜中
理由:
- XPUBの統計配信はベストエフォート: 統計メッセージの一部が失われても負荷分散の精度が一時的に低下するだけ。定期的に再送されるため自己回復する
- Wave開始指示の喪失: エンジンが一時的にidle状態のまま留まる可能性がある。ただしフロントエンド側からの新リクエスト送信で再度waveが開始されるため、長時間のデッドロックにはならない
- 接続確認: 全サブスクライバのsubscription受信を待ってから通信開始
参照: target/vllm/vllm/v1/engine/coordinator.py:189-198 (サブスクリプション待ち)
3. ShmRingBufferフォールバック
リスク: 低
理由:
- XPUB/SUBだがHWM未設定(デフォルト1000): 理論上、非常に大きなメッセージが連続すると滞留可能
- 同一ホスト内IPC: ネットワーク障害なし
- 頻度が低い: 24MiB超のメッセージは稀(通常のSchedulerOutput、ModelRunnerOutputは小さい)
- オーバーフロー自体がレアパス: 通常はShmRingBuffer直接で完結
メッセージ喪失時の影響: Worker側でRPCレスポンスが受信できず、EngineCore側でハングする可能性
4. KV Cache Events
リスク: 中(設計上許容)
理由:
- PUBにHWM=100,000: サブスクライバが遅い場合、メッセージがドロップされる
- TCP経由: ネットワーク障害のリスクあり
- リプレイ機能で緩和: シーケンス番号ギャップを検出し、ROUTERソケット経由で過去イベントを再取得可能
- 外部サービス向け: vLLMのコア動作には影響しない。あくまでKVイベントの外部通知
メッセージ喪失時の影響: 外部キャッシュマネージャがイベントを見逃す。リプレイで回復可能(バッファ範囲内)
5. KV Transfer コネクタ
リスク: 中
理由:
- TCP経由(ノード間通信): ネットワーク障害のリスクあり
- NIXL: RCVTIMEO設定あり: 1000msまたは5000msのタイムアウト。
zmq.Again例外をキャッチしてリトライロジックを実装 - P2P NCCL: Pollerでの待機、明示的なタイムアウトなし(ブロッキング)
- Mooncake: DEALER側にlinger=0設定。タイムアウト付きで転送
メッセージ喪失時の影響:
- ハンドシェイク失敗→コネクタ初期化失敗→ログエラー、該当リクエストは通常の計算パスにフォールバック
- 転送通知失敗→送信側がブロックをタイムアウト解放→受信側はprefillを再実行
信頼性設計のまとめ
vLLMのZMQ使用における信頼性戦略:
| 戦略 | 適用箇所 | 説明 |
|---|---|---|
| HWM=0(無制限バッファ) | Frontend↔EngineCore | メッセージドロップを完全に防止 |
| IPC優先 | 同一ホスト内通信 | ネットワーク障害を排除 |
| プロセス死活監視 | Frontend→EngineCore | sentinelによる即座のクラッシュ検出 |
| ENGINE_CORE_DEAD通知 + linger | EngineCore→Frontend | 異常終了の明示的通知を保証 |
| ハンドシェイク | 起動時 | 通信確立の確認後に運用開始 |
| リプレイ機能 | KV Events | メッセージドロップ後の回復手段 |
| タイムアウト + リトライ | KV Transfer | ネットワーク障害時のフォールバック |
| ベストエフォート + 自己回復 | DP Coordinator | 統計は定期再送、waveは再トリガー |
結論: vLLMはコア通信パスでは実質的にメッセージ喪失が起きない設計(HWM=0 + IPC + プロセス監視)を採用し、補助的な通信パスではベストエフォート + リカバリ機構(リプレイ、タイムアウト、再トリガー)で対処している。ZMQの「到達保証なし」の弱点は、使用パターンの選択(IPC、HWM=0)とアプリケーション層の監視で効果的に緩和されている。
ソケットタイプ使用一覧
| ソケットタイプ | 使用箇所 | 方向 | 特徴 |
|---|---|---|---|
ROUTER | Frontend input, NIXL server, P2P server, Mooncake server, MoRIIO server, KV Events replay, ハンドシェイク | bind(サーバー) | identityベースルーティング |
DEALER | EngineCore input, NIXL client, P2P client, Mooncake client, MoRIIO client | connect(クライアント) | 透過的なidentity送信 |
PUSH | EngineCore output, EngineCore→Coordinator | connect | 単方向、ブロック型 |
PULL | Frontend output, Coordinator←EngineCore | bind | 単方向、フェアキューイング |
XPUB | Coordinator→Frontend, Coordinator→EngineCore, MessageQueue writer | bind | サブスクリプション可視化 |
XSUB | EngineCore←Coordinator, Frontend←Coordinator | connect | 明示的サブスクリプション |
SUB | MessageQueue reader | connect | 自動サブスクリプション |
PUB | KV Events | bind | ブロードキャスト、HWMドロップ |
PAIR | Frontend内部(shutdown通知、first_req通知) | bind/connect | 排他的1:1ペア |
REQ | NIXL ハンドシェイクclient | connect | 同期的リクエスト/レスポンス |
参照
| ファイル | 行 | 内容 |
|---|---|---|
target/vllm/vllm/v1/engine/core_client.py | L510-514 | Frontend側ZMQソケット作成(ROUTER/PULL) |
target/vllm/vllm/v1/engine/core_client.py | L539-549 | ROUTER初期メッセージ待ち(poll + タイムアウト) |
target/vllm/vllm/v1/engine/core_client.py | L581-587 | MessageTracker管理(ゼロコピー参照保持) |
target/vllm/vllm/v1/engine/core_client.py | L684-720 | SyncMPClientの出力処理スレッド(Poller + PAIR shutdown) |
target/vllm/vllm/v1/engine/core_client.py | L877-901 | AsyncMPClientの出力処理タスク |
target/vllm/vllm/v1/engine/core_client.py | L1080-1186 | DPClient統計購読(XSUB + PAIR first_req) |
target/vllm/vllm/v1/engine/core.py | L870-920 | EngineCore側ハンドシェイク(DEALER→ROUTER) |
target/vllm/vllm/v1/engine/core.py | L1186-1265 | EngineCore入力スレッド(DEALER + XSUB + Poller) |
target/vllm/vllm/v1/engine/core.py | L1267-1335 | EngineCore出力スレッド(PUSH, tracker, linger=4000) |
target/vllm/vllm/v1/engine/coordinator.py | L113-395 | DPCoordinator(XPUB×2 + PULL, Wave調整) |
target/vllm/vllm/v1/engine/utils.py | L937-1091 | ハンドシェイクプロトコル(HELLO→metadata→READY) |
target/vllm/vllm/utils/network_utils.py | L260-313 | make_zmq_socket(HWM=0, buf_size, IPv6対応) |
target/vllm/vllm/distributed/device_communicators/shm_broadcast.py | L280-403 | MessageQueue(XPUB/SUB, ShmRingBufferフォールバック) |
target/vllm/vllm/distributed/device_communicators/shm_broadcast.py | L571-594 | enqueue(オーバーフロー判定→ZMQ送信) |
target/vllm/vllm/distributed/kv_events.py | L270-400 | ZmqEventPublisher(PUB + ROUTER replay) |
target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/nixl_connector.py | L615-618 | NIXL ROUTER(RCVTIMEO=1000ms) |
target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/p2p/p2p_nccl_engine.py | L124-130 | P2P NCCL ROUTER/DEALER |
関連ドキュメント
- プロセスアーキテクチャ(TP=2構成) — ShmRingBuffer、通信方式選択理由の詳細
- Executorコンポーネント — MessageQueue、WorkerProc busy loop
- EngineCoreClientコンポーネント — Frontend側の通信クライアント階層
- KV Transferコンポーネント — KVConnectorBase_V1、各コネクタの概要
用語集
PagedAttention
KVキャッシュをOSの仮想メモリページングに着想を得て、固定サイズのブロック単位で管理する技術。連続したGPUメモリ確保が不要になり、メモリ断片化を大幅に抑制する。SOSP 2023論文で提案。
参照: target/vllm/csrc/attention/ (カーネル実装)
Continuous Batching
リクエストの到着・完了に応じてバッチを動的に更新する手法。固定バッチサイズと異なり、GPU稼働率を最大化できる。vLLMのSchedulerが担う。
Prefill
プロンプト入力トークン全体を処理してKVキャッシュに書き込む最初のフェーズ。計算量が多く、GPUの並列性を活かしやすい。
Decode
生成済みコンテキストのKVキャッシュを参照しながら次のトークンを1つずつ逐次生成するフェーズ。メモリバウンドになりやすい。
Chunked Prefill
Prefillフェーズをチャンクに分割してDecodeフェーズと交互実行する手法。長いプロンプトがDecodeのレイテンシを増加させるのを防ぐ。
EngineCore
vLLMの推論ループの内側コンポーネント。別プロセス(EngineCoreProc)として動作し、ZeroMQソケットで上位エンジン層と通信する。Scheduler、KVCacheManager、Executorを統括する。
参照: target/vllm/vllm/v1/engine/core.py:79 (EngineCore)
KVCacheManager
KVキャッシュブロックの割り当て・解放・プレフィックスキャッシュを管理するクラス。BlockPoolを内部で使用する。
参照: target/vllm/vllm/v1/core/kv_cache_manager.py:94 (KVCacheManager)
KVCacheBlock
PagedAttentionで管理するKVキャッシュの最小単位。固定サイズ(block_sizeトークン分)のGPUメモリブロック。
BlockPool
KVCacheBlockの空きブロックをプール管理するクラス。ブロックの割り当て・返却を効率的に行う。
参照: target/vllm/vllm/v1/core/block_pool.py
VllmConfig
全設定を集約するトップレベルクラス。ModelConfig、CacheConfig、SchedulerConfig、ParallelConfig等を内包する。
参照: target/vllm/vllm/config/vllm.py
GPUModelRunner
GPU上でモデルのフォワードパスを実際に実行するクラス。LoRA、KVConnector、ECConnectorのMixinを持つ。
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:329 (GPUModelRunner)
Executor
Worker群を管理する抽象層。シングルプロセス(UniProcExecutor)、マルチプロセス(MultiprocExecutor)、Ray分散(RayDistributedExecutor)の実装がある。
参照: target/vllm/vllm/v1/executor/abstract.py
Worker
1つのGPU(またはCPU/XPU)デバイスを担当するプロセス。GPUModelRunnerを保持し、Executorから呼び出される。
参照: target/vllm/vllm/v1/worker/gpu_worker.py:70 (Worker)
Speculative Decoding
ドラフトモデル(小さいモデル)で複数トークンを仮生成し、メインモデルで一括検証することで推論を高速化する手法。
参照: target/vllm/vllm/v1/spec_decode/
LoRA (Low-Rank Adaptation)
少量の追加パラメータでLLMをファインチューニングする手法。vLLMは複数LoRAの動的切替(Multi-LoRA)をランタイムでサポートする。
参照: target/vllm/vllm/lora/
KV Transfer
複数のvLLMインスタンス間またはストレージ間でデコーダKVキャッシュを転送するプラグインフレームワーク。KVConnectorBase_V1抽象基底クラス(7 abstractメソッド)と KVConnectorFactory(10個の登録済みコネクタ)で構成。Scheduler側(外部キャッシュ問い合わせ)とWorker側(レイヤー別非同期ロード/セーブ)の2ロール分離。ECConnector(エンコーダキャッシュ用)とは完全に独立した系統。
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/base.py:147 (KVConnectorBase_V1), docs/src/components/kv-transfer/summary.md
KVConnectorBase_V1
KV Transferのプラグイン基底クラス。Worker側4メソッド(start_load_kv, wait_for_layer_load, save_kv_layer, wait_for_save)とScheduler側3メソッド(get_num_new_matched_tokens, update_state_after_alloc, build_connector_meta)を定義する。KVConnectorMetadataでScheduler→Worker間の通信を行う。
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/base.py:147
KVConnectorFactory
KVコネクタの登録・発見・生成を行うファクトリクラス。遅延ロードパターン(module_path + class_name)で10個のコネクタを登録。kv_connector_module_path設定で動的ロードも可能。
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/factory.py:27
KVTransferConfig
KV Transferの設定クラス。kv_connector(コネクタ名)、kv_role(producer/consumer/both)、kv_connector_extra_config(コネクタ固有設定)等を保持。
参照: target/vllm/vllm/config/kv_transfer.py:17
KVConnectorModelRunnerMixin
GPUModelRunnerにミックスインされるKVコネクタ統合クラス。_get_kv_connector_output()コンテキストマネージャでbind→start_load→yield→wait_for_save→get_finished→clearのライフサイクルを管理する。
参照: target/vllm/vllm/v1/worker/kv_connector_model_runner_mixin.py:40
LMCache
vLLMと統合可能な外部KVキャッシュライブラリ。チャンク単位(デフォルト256トークン)でKVキャッシュを保存し、3層ストレージ階層(CPU→Disk→Remote)を持つ。15+のリモートコネクタ(Redis, S3, FS等)をサポート。Disaggregated Serving(P/D分離)にも対応。
参照: target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/lmcache_connector.py:72, docs/src/investigations/lmcache-integration.md
Disaggregated Prefill (P/D分離)
PrefillフェーズとDecodeフェーズを異なるvLLMインスタンスで実行するアーキテクチャ。Producerインスタンスがプリフィルを実行してKVキャッシュをKV Transfer経由で転送し、ConsumerインスタンスがKVキャッシュをロードしてデコードのみ実行する。
WAITING_FOR_REMOTE_KVS
KV Transferの非同期ロード中のリクエスト状態。Schedulerがブロックを割り当て後、Worker側のKVロード完了を待つ間この状態に置かれる。Worker側コネクタのget_finished()で受信完了が報告されると、WAITING状態に戻されて次のスケジューリングサイクルでRUNNINGに昇格する。
KV Cache Events
KVキャッシュのブロック保存・削除を外部システムに通知するイベントシステム。BlockStored、BlockRemoved、AllBlocksClearedの3イベント型。KVEventAggregatorで全Worker共通イベントを集約し、EventPublisher(ZMQ等)で配信。
参照: target/vllm/vllm/distributed/kv_events.py:49-84
CacheEngineKey (LMCache)
LMCacheのチャンクを一意に識別するキー。model_name、world_size、worker_id、chunk_hash(トークン列のプレフィックスハッシュ)、dtype、タグで構成。
参照: target/LMCache/lmcache/utils.py:330
Multimodal (マルチモーダル)
テキスト以外の入力(画像・動画・音声)を扱うモデル機能。vllm/multimodal/ にプロセッサ・レジストリ等が実装されている。Gemma3等のマルチモーダルモデルが対応。
参照: target/vllm/vllm/multimodal/
Unified Compute Model
vLLM v1のSchedulerが採用するスケジューリングアプローチ。PrefillフェーズとDecodeフェーズを明示的に区別せず、各リクエストのnum_computed_tokens(計算済みトークン数)が目標に追いつくまでトークンを割り当てる。これにより、Chunked Prefill、Prefix Caching、Speculative Decodingを統一的に扱える。
参照: target/vllm/vllm/v1/core/sched/scheduler.py:322 (コメント)
collective_rpc
Executor層が全Workerに対して同一メソッドを実行するRPCパターン。メソッド名(文字列)または関数を受け取り、全Workerで並列実行後、出力ランクのWorkerの結果を返す。non_block=TrueでFuture返却も可能。
参照: target/vllm/vllm/v1/executor/abstract.py:180 (collective_rpc)
ExecuteModelState
GPUModelRunnerの2フェーズ実行パターンで使用される一時状態。execute_model()がlogitsやhidden_statesなどのGPUテンソルを保存し、sample_tokens()が復元してサンプリングを行う。NamedTuple。
参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:313 (ExecuteModelState)
OutputProcessor
フロントエンドプロセスで動作し、EngineCoreOutputをRequestOutputに変換するコンポーネント。インクリメンタルデトークナイズ、停止文字列判定、logprobs処理を行う。
参照: target/vllm/vllm/v1/engine/output_processor.py:73 (OutputProcessor)
IncrementalDetokenizer
トークンIDからテキストへのインクリメンタル変換を行うクラス。FastIncrementalDetokenizer(HF DecodeStream)とSlowIncrementalDetokenizer(Python実装)の2種がある。
参照: target/vllm/vllm/v1/engine/detokenizer.py:30 (IncrementalDetokenizer)
RequestOutputKind
出力モードを定義するEnum。CUMULATIVE(毎回全出力)、DELTA(差分のみ、ストリーミング向け)、FINAL_ONLY(完了時のみ)の3値。
参照: target/vllm/vllm/sampling_params.py:108 (RequestOutputKind)
mm_cache (マルチモーダルキャッシュ)
マルチモーダル入力(画像エンコーダ出力等)のキャッシュ機構。同一画像の繰り返し処理を避けるため、エンコーダ出力をキャッシュする。
参照: target/vllm/vllm/v1/worker/gpu/mm/encoder_runner.py
FreeKVCacheBlockQueue
空きKVキャッシュブロックをLRU順序で管理する双方向リンクリスト。Pythonのdequeではなく独自実装を採用し、O(1)の中間要素削除をサポートする。センチネルノード(fake_head/fake_tail)を使用。
参照: target/vllm/vllm/v1/core/kv_cache_utils.py:156
BlockHashWithGroupId
BlockHash(ブロックのハッシュ値)にKVキャッシュグループID(4バイトBE)を結合したバイト列。Tuple生成を避けてGC負荷を低減する。プレフィックスキャッシュのキーとして使用。
参照: target/vllm/vllm/v1/core/kv_cache_utils.py:39
null_block
BlockPoolが保持する特殊なKVCacheBlock(block_id=0, is_null=True)。Sliding Window Attentionのウィンドウ外位置やMambaのスキップ位置を埋めるプレースホルダ。物理メモリを消費せず、解放・Eviction対象外。
参照: target/vllm/vllm/v1/core/block_pool.py:174
KVCacheCoordinator
複数のKVキャッシュグループ(異なるアテンションタイプのレイヤー群)を統括する抽象クラス。NoPrefixCache、Unitary(単一グループ)、Hybrid(複数グループ)の3実装がある。
参照: target/vllm/vllm/v1/core/kv_cache_coordinator.py:28
SingleTypeKVCacheManager
1種類のアテンションタイプのKVキャッシュ管理ロジックを担当する抽象基底クラス。FullAttention、SlidingWindow、ChunkedLocalAttention、Mamba、CrossAttention、SinkFullAttentionの7実装がある。
参照: target/vllm/vllm/v1/core/single_type_kv_cache_manager.py:24
Cascade Attention
全リクエストで共有される共通プレフィックスの再計算をスキップする最適化。get_num_common_prefix_blocks()で共通ブロック数を判定し、アテンション計算から除外する。
Sliding Window Attention
各トークンが直近のN個のトークンにのみアテンションするメカニズム。ウィンドウ外のKVキャッシュブロックはnull_blockで置換されメモリを節約する。
Attention Sink (StreamingLLM)
先頭の少数トークン(sink tokens)のKVキャッシュを常に保持しつつ、中間トークンを捨てて長いシーケンスを処理する手法。SinkFullAttentionManagerが実装。
MultiModalFeatureSpec
マルチモーダル入力1つ分のメタデータとテンソルデータを保持するデータクラス。data(処理済みテンソル、キャッシュヒット時はNone)、identifier(エンコーダキャッシュ用ハッシュ)、mm_position(PlaceholderRange)、modality(“image“等)を含む。
参照: target/vllm/vllm/multimodal/inputs.py:337
PlaceholderRange
プロンプト内のマルチモーダルプレースホルダーの位置情報。offset(開始位置)、length(トークン数)、is_embed(埋め込みマスク)を保持。
参照: target/vllm/vllm/multimodal/inputs.py:170
MultiModalHasher
マルチモーダルデータのコンテンツベースハッシュを計算するクラス。PIL Image、Tensor、ndarray等を決定的にシリアライズし、blake3(デフォルト)でハッシュ化する。
参照: target/vllm/vllm/multimodal/hasher.py:50
ProcessorCache (MM)
フロントエンド(P0)でHF Processor処理結果をキャッシュする仕組み。4種類の実装(processor_only/lru/shm/none)があり、P0-P1間のキャッシュEviction順序を同期させることでIPCなしにキャッシュ状態を推定できる。
参照: target/vllm/vllm/multimodal/cache.py
EncoderCacheManager
バックエンド(P1)でビジョンエンコーダ出力のライフサイクルを管理するクラス。リファレンスカウント方式で複数リクエスト間のキャッシュ共有を実現し、FIFO順の遅延Evictionを行う。
参照: target/vllm/vllm/v1/core/encoder_cache_manager.py:17
SiglipVisionModel
SIGLIP(Sigmoid Loss for Language Image Pre-training)ベースのViTビジョンエンコーダ。Gemma3のビジョンタワーとして使用される。パッチ埋め込み + 位置埋め込み → Transformer Encoder(双方向Attention)の構造。
参照: target/vllm/vllm/model_executor/models/siglip.py:848
Pan-and-Scan
アスペクト比が大きい画像を複数のクロップに分割して詳細認識を向上させるGemma3の仕組み。V1では簡略化されたアテンションパターンのため最適でない結果になりうる。
参照: target/vllm/vllm/model_executor/models/gemma3_mm.py:109
ファイル索引
LMCache
KVキャッシュの保存・共有・再利用ライブラリ LMCache のコードリーディング。
- ソースコード:
target/LMCache/ - 現在のPhase: Phase 0a(オリエンテーション)
データフロー
深度: [MEDIUM] / 確信度: [VERIFIED] 最終更新: 2026-02-16(Phase 1 セッション2)
本ドキュメントはLMCacheのKVキャッシュ store / retrieve パスを追跡する。
Store パス概要
vLLMのattentionレイヤー実行中に、KVキャッシュをGPUからCPU(および後段ストレージ)に退避するパス。
レイヤーワイズ方式(use_layerwise=True)が主要パスであり、各attentionレイヤーの実行直後にそのレイヤーのKVデータを転送する。
sequenceDiagram
participant vLLM as vLLM Attention Layer
participant Adapter as V1Impl (adapter)
participant Engine as LMCacheEngine
participant TDB as TokenDatabase
participant GPU as GPUConnector
participant SM as StorageManager
participant CPU as LocalCPUBackend
Note over vLLM,Adapter: Layer 0 開始
vLLM->>Adapter: save_kv_layer(layer_name, kv_layer, attn_metadata)
Note over Adapter: layer==0 のとき、各リクエストに対して Generator 生成
Adapter->>Engine: store_layer(token_ids, mask, kvcaches, slot_mapping, ...)
activate Engine
Engine->>TDB: process_tokens(tokens, mask)
TDB-->>Engine: [(start, end, CacheEngineKey), ...]
Engine->>SM: batched_allocate(shape, dtype, batch_size=num_layers)
SM-->>Engine: List[MemoryObj] × num_layers
Engine->>GPU: batched_from_gpu(memory_objs, starts, ends, ...)
Note over GPU: GPU Generator 初期化
GPU-->>Engine: Generator (primed)
Note over Engine: yield (Layer 0 の DMA 準備完了)
deactivate Engine
Adapter->>Engine: next(generator) — Layer 0
activate Engine
Engine->>GPU: next(mem_obj_generator)
Note over GPU: lmc_ops.single_layer_kv_transfer<br/>(paged GPU → buffer → pinned CPU)
GPU-->>Engine: yield
Engine->>SM: batched_put(keys[0], memory_objs[0])
SM->>CPU: batched_submit_put_task(keys, objs)
Note over CPU: hot_cache[key] = memory_obj
Note over Engine: yield (Layer 0 完了)
deactivate Engine
Note over vLLM,Adapter: Layer 1 以降も同じパターン繰り返し
vLLM->>Adapter: save_kv_layer(...)
Adapter->>Engine: next(generator) — Layer N
Note over Engine: GPU転送 → StorageManager.batched_put
各コンポーネントの役割
1. LMCacheConnectorV1Impl(adapter)
参照: target/LMCache/lmcache/integration/vllm/vllm_v1_adapter.py:964 (save_kv_layer)
vLLMのKVConnectorBase_V1.save_kv_layer()フックから呼ばれるアダプタ。
LMCacheConnectorV1Dynamicは純粋な委譲シェルであり、実装はV1Implに集約。
Layer 0での処理:
connector_metadata.requestsを走査し、save_spec.can_saveがTrueのリクエストを処理skip_leading_tokensをLMCacheのchunk_size(256)の倍数に切り下げてマスク境界を整合store_maskを構築:プレフィックス部分=False、新規部分=TrueLMCacheEngine.store_layer()を呼んでGeneratorを取得、self.layerwise_storersに追加- 最初のリクエストのみ
sync=TrueでCUDAストリームを同期
全レイヤー共通: self.layerwise_storers内の全Generatorをnext()で1ステップ進める。
2. LMCacheEngine.store_layer()
参照: target/LMCache/lmcache/v1/cache_engine.py:528
Generator関数であり、呼び出し側(adapter)が1レイヤーごとにnext()で進める。
初期化フェーズ(最初のyieldまで):
TokenDatabase.process_tokens()でトークン列をチャンク分割し、各チャンクのCacheEngineKeyを取得StorageManager.contains()で既存チャンクをスキップ(layer 0のキーで判定)StorageManager.batched_allocate()で各チャンク×全レイヤー分のMemoryObjを確保- チャンク×レイヤー → レイヤー×チャンクに転置
GPUConnector.batched_from_gpu()でGPU転送Generatorを生成・prime
レイヤーループ(num_layers回yield):
yield → next(mem_obj_generator) → batched_put(keys[layer_id], memory_objs[layer_id])
各レイヤーで「GPU→CPU DMA」→「ストレージ書き込み」を実行。
重要: メモリ確保失敗時(batched_allocateがNone)はbreakで即座にstore中止。yieldだけ行ってストレージには書かない。
3. ChunkedTokenDatabase.process_tokens()
参照: target/LMCache/lmcache/v1/token_database.py:309
トークン列をチャンク(デフォルト256トークン)に分割し、プレフィックスチェーンハッシュを計算。
ハッシュアルゴリズム: vLLMと完全に同一。
vllm.utils.hashing.get_hash_fn_by_name("sha256_cbor")を直接利用- NONE_HASHも
vllm.v1.core.kv_cache_utils.NONE_HASHから取得 - ハッシュ入力:
(prefix_hash, token_tuple, extra_keys)
マスク処理: maskのFalse数(=already-cached prefix長)がchunk_sizeの倍数であることを検証。False区間のチャンクはスキップ。
CacheEngineKey生成: _make_key_by_hash()で(model_name, world_size, worker_id, chunk_hash, kv_dtype, request_configs)の6タプルを構築。その後split_layers()でレイヤーIDを付与したLayerCacheEngineKeyに分割。
4. VLLMPagedMemLayerwiseGPUConnector.batched_from_gpu()
参照: target/LMCache/lmcache/v1/gpu_connector/gpu_connectors.py:1212
GPU上のページドKVキャッシュからCPU上のMemoryObjにデータを転送するGenerator関数。
2段転送パス(use_gpu=True時):
- Paged GPU → 中間GPUバッファ:
lmc_ops.single_layer_kv_transfer()(CUDAカーネル)でslot_mappingに基づきscatter→gatherコピー - GPUバッファ → Pinned CPU:
memory_obj.tensor.copy_(..., non_blocking=True)で非同期DMA
直接転送パス(use_gpu=False時):
lmc_ops.single_layer_kv_transfer()でpaged GPUから直接pinned CPUへ(チャンク単位)
CUDAストリーム: self.store_stream(専用ストリーム)を使用し、計算ストリームとオーバーラップ可能。sync=Trueの場合のみstore_stream.synchronize()で同期。
出力形式: MemoryFormat.KV_T2D = [num_tokens, 2, hidden_dim](token-major、K/Vインターリーブ)。MLAの場合はKV_MLA_FMT = [num_tokens, hidden_dim]。
5. StorageManager.batched_put()
参照: target/LMCache/lmcache/v1/storage_backend/storage_manager.py:388
登録された全ストレージバックエンドにデータを配布するディスパッチャ。
処理フロー:
allocator_backend(通常LocalCPUBackend)の元データをそのまま利用OrderedDict順に全バックエンド(L1→L2→L3)を走査- 異なるallocatorを持つバックエンドには
allocate_and_copy_objects()で新たにメモリ確保+コピー - 各バックエンドの
batched_submit_put_task()を呼び出し - 最後にref_countをデクリメント
注意: put()メソッドは非推奨(RuntimeErrorを投げる)。batched_put()が唯一のエントリポイント。
6. LocalCPUBackend.submit_put_task()
参照: target/LMCache/lmcache/v1/storage_backend/local_cpu_backend.py:141
同期実行(バックグラウンドスレッドなし)。cpu_lock下で以下を実行:
- 既存キーの重複チェック
memory_obj.ref_count_up()でrefcount増加hot_cache[key] = memory_objで保存cache_policy.update_on_put(key)でEvictionポリシー更新(LRU: OrderedDictの末尾に移動、等)- 必要に応じてcontrollerへADMITメッセージ送信(
batched_msg_sender経由)
パイプライン動作の詳細
store_layerとbatched_from_gpuは2つの入れ子Generatorでパイプライン動作する:
store_layer Generator: [初期化] → yield → [L0転送+保存] → yield → [L1転送+保存] → yield → ...
batched_from_gpu Generator: [初期化] → yield → [L0 DMA] → yield → [L1 DMA] → yield → ...
タイミング(num_layers=Nの場合):
store_layerはN+1回yield(初期化1回 + レイヤーN回)batched_from_gpuはN+1回yield(初期化prime + レイヤーN回)- adapterは合計
N回next()を呼ぶ(各attentionレイヤー実行後)
パイプラインのステップ: Layer Lのnext()呼び出しで、batched_from_gpuがLayer LのDMAを実行し、store_layerがLayer LのStorageManager書き込みを行う。つまりDMAとストレージ書き込みは同一レイヤーで連続実行される。
データ構造
| 構造 | 型 | 説明 |
|---|---|---|
| CacheEngineKey | (model_name, world_size, worker_id, chunk_hash, kv_dtype, request_configs) | チャンク単位のキー(レイヤー横断) |
| LayerCacheEngineKey | CacheEngineKey + layer_id | レイヤー単位のキー |
| MemoryObj | pinned CPU tensor wrapper | ref_count管理、MemoryObjMetadata付き |
| MemoryFormat.KV_T2D | [num_tokens, 2, hidden_dim] | レイヤーワイズ形式(token-major) |
| MemoryFormat.KV_MLA_FMT | [num_tokens, hidden_dim] | MLA形式(K/V統合) |
| store_mask | torch.Tensor[bool] | False=キャッシュ済みprefix、True=新規トークン |
| slot_mapping | torch.Tensor[long] | トークン位置→vLLMページドメモリのflat slot |
| hot_cache | OrderedDict[CacheEngineKey, MemoryObj] | L1 CPUキャッシュ(Evictionポリシー付き) |
Retrieve パス概要
KVキャッシュをストレージ(CPU/Disk/Remote)からGPUのvLLMページドメモリに復元するパス。 2フェーズ設計: Scheduler側のlookup(ヒット判定+prefetch指示)と、Worker側のload(実際のGPU転送)に分離。
Scheduler→Worker間の情報伝達
sequenceDiagram
participant Sched as V1Impl (Scheduler側)
participant LC as LookupClient
participant Worker as V1Impl (Worker側)
participant Engine as LMCacheEngine
Note over Sched: vLLM Scheduler.schedule() から呼ばれる
Sched->>LC: lookup(token_ids, req_id)
LC-->>Sched: num_external_hit_tokens
Note over Sched: LoadSpec(vllm_cached, lmcache_cached) を生成
Sched->>Sched: update_state_after_alloc() → can_load=True
Sched->>Sched: build_connector_meta() → ReqMeta(load_spec) を構築
Note over Sched: ConnectorMetadata を SchedulerOutput に添付
Note over Worker: Forward開始時
Worker->>Engine: start_load_kv(forward_context)
Note over Engine: Bulk or Layerwise retrieve 実行
LookupClient の動作
参照: target/LMCache/lmcache/v1/lookup_client/lmcache_lookup_client.py:28
LMCacheLookupClientはvLLMのSchedulerプロセスで動作する。LMCacheEngine(Worker側)とはZMQ IPC(REQ/REP)で通信。
処理フロー:
process_tokens()でトークン列をチャンクハッシュに分割- ハッシュ列をmsgpackシリアライズし、ZMQで
LookupServerに送信 LookupServer(Worker側)がStorageManager.contains()で存在チェック- ヒットトークン数を返却
キャッシュ: 同一リクエストの2回目以降のlookupはreqs_status辞書から即座に返す。
Retrieve パスの2モード
| モード | 条件 | エントリポイント | 特徴 |
|---|---|---|---|
| Bulk | use_layerwise=False(デフォルト) | LMCacheEngine.retrieve() | 全レイヤー一括取得→一括GPU転送 |
| Layerwise | use_layerwise=True | LMCacheEngine.retrieve_layer() | レイヤー単位Generator、パイプライン可能 |
Bulk Retrieve パス
sequenceDiagram
participant Adapter as V1Impl (Worker側)
participant Engine as LMCacheEngine
participant TDB as TokenDatabase
participant SM as StorageManager
participant CPU as LocalCPUBackend
participant GPU as GPUConnector (V2)
Adapter->>Engine: retrieve(tokens, mask, kvcaches, slot_mapping, ...)
activate Engine
Engine->>TDB: process_tokens(tokens, mask)
TDB-->>Engine: [(start, end, CacheEngineKey), ...]
alt async_loading == True
Note over Engine: event_managerからprefetch済みMemoryObjを取得
Engine->>Engine: _async_process_tokens_internal()
else sync loading
Engine->>SM: get_block_mapping(chunk_infos)
SM-->>Engine: {backend_name: [(key, start, end)]}
Engine->>SM: batched_get(keys, location)
SM->>CPU: batched_get_blocking(keys)
CPU-->>SM: List[MemoryObj](ref_count_up済み)
SM-->>Engine: List[MemoryObj]
end
Engine->>GPU: batched_to_gpu(memory_objs, starts, ends, slot_mapping=...)
Note over GPU: load_stream上で全チャンクをGPU転送
GPU->>GPU: lmc_ops.multi_layer_kv_transfer(memobj→paged KV)
GPU-->>Engine: 完了
Note over Engine: memory_obj.ref_count_down() で解放
Engine-->>Adapter: ret_mask (bool tensor)
deactivate Engine
_process_tokens_internal() の詳細
参照: target/LMCache/lmcache/v1/cache_engine.py:1527
process_tokens()でチャンク分割・ハッシュ計算StorageManager.get_block_mapping()でチャンクの所在バックエンドを特定- 各バックエンドの
batched_contains()をprefix match方式で呼び出し - チャンクを所在バックエンドごとにグルーピング
- 各バックエンドの
- バックエンドごとに
batched_get()でMemoryObjを取得 - 取得失敗時は
last_failed_block_start以降のret_maskをFalseに戻し、チャンクリストを切り詰め
_async_process_tokens_internal() の詳細
参照: target/LMCache/lmcache/v1/cache_engine.py:1463
非同期プリフェッチ済みの結果をevent_managerから取得するパス。
event_manager.pop_event(EventType.LOADING, req_id)でprefetch結果のFutureを取得future.result()でlist[list[tuple[CacheEngineKey, MemoryObj]]](tier×chunk)を取得process_tokens()で再度チャンク分割し、memory_obj_mapからマッチングしてチャンクリストを構築- 未使用の
MemoryObjはref_count_down()で即座に解放
StorageManager.batched_get() のwrite-back
参照: target/LMCache/lmcache/v1/storage_backend/storage_manager.py:484
リモートバックエンド(Disk/Remote)からデータを取得した場合、自動的にLocalCPUBackendにwrite-backする。
LocalCPUBackend以外から取得 &&LocalCPUBackendが存在 && 全MemoryObjがnon-None →batched_submit_put_task()でL1にコピー
Layerwise Retrieve パス
sequenceDiagram
participant Adapter as V1Impl (Worker側)
participant Engine as LMCacheEngine
participant TDB as TokenDatabase
participant SM as StorageManager
participant GPU as GPUConnector (Layerwise)
Note over Adapter: start_load_kv() 内
Adapter->>Engine: retrieve_layer(tokens, mask, kvcaches, slot_mapping, sync)
activate Engine
Engine->>TDB: process_tokens(tokens, mask)
TDB-->>Engine: [(start, end, CacheEngineKey), ...]
Note over Engine: contains(layer0_key) でヒット判定 + location統一チェック
Engine->>SM: layerwise_batched_get(keys_layer_major, location)
Note over SM: Layer 0 の get_non_blocking を asyncio.create_task() で投入
Engine->>GPU: batched_to_gpu(starts, ends, ...) → mem_obj_consumer Generator 生成
GPU-->>Engine: mem_obj_consumer primed (yield)
Engine-->>Adapter: yield torch.sum(ret_mask) — Layer 0 のヒット数
deactivate Engine
Note over Adapter: next(retriever) で Layer 0 データ受領
Adapter->>Engine: next(retriever)
activate Engine
Note over Engine: Layer 0 の task.result() を取得
Engine->>GPU: mem_obj_consumer.send(mem_objs_layer0)
Note over GPU: CPU→GPUバッファ copy(load_stream)
SM-->>Engine: Layer 1 の task yield
Engine-->>Adapter: yield None
deactivate Engine
Note over Adapter: wait_for_layer_load(layer_name) で同期
Adapter->>Engine: next(retriever)
activate Engine
Note over Engine: Layer N-1 処理...
Engine-->>Adapter: yield ret_mask(最終レイヤー後)
deactivate Engine
retrieve_layer() の Generator 構造
参照: target/LMCache/lmcache/v1/cache_engine.py:851
num_layers + 3回yieldする(ヒットあり時):
- yield 0:
torch.sum(ret_mask)— ヒットトークン数(sglang統合向け) - yield 1 ~ N-1:
None— 各レイヤーのGPU転送進行中 - yield N:
None— 最終レイヤー処理中 - yield N+1:
next(mem_obj_consumer)で同期 - yield N+2:
ret_mask— 最終結果
各レイヤーで:
next(get_generator)でStorageManagerから非同期取得したFutureを受け取るtask.result()でList[MemoryObj]を取得(ブロッキング)mem_obj_consumer.send(mem_objs_layer)でGPUコネクタにデータを渡すMemoryObj.ref_count_down()は全レイヤー完了後にバッチで実行
Layerwise GPUConnector.batched_to_gpu() のパイプライン
参照: target/LMCache/lmcache/v1/gpu_connector/gpu_connectors.py:683
VLLMBufferLayerwiseGPUConnectorはnum_layers + 2回のイテレーションで3段パイプラインを実行:
| イテレーション i | 操作1: paged書き込み | 操作2: RoPE補正+gap zeroing | 操作3: CPU→GPU load |
|---|---|---|---|
| i = 0 | — | — | yieldでmem_objs_layer0受領、load_stream上でcopy |
| i = 1 | — | Layer 0のRoPE補正 | yieldでmem_objs_layer1受領、load_stream上でcopy |
| i = 2 | Layer 0をpagedメモリに書き込み | Layer 1のRoPE補正 | yieldでmem_objs_layer2受領 |
| … | Layer i-2 | Layer i-1 | Layer i |
| i = N | Layer N-2 | Layer N-1 | yield(同期用、データなし) |
| i = N+1 | Layer N-1 | — | — |
ダブルバッファ: compute_gpu_buffer_objとload_gpu_buffer_objをping-pongして、RoPE計算とDMAをオーバーラップ。
RoPE位置補正: cache_positions=Trueの場合、保存時の位置と現在の位置の差分でfused_rotary_emb()を適用。保存時位置はMemoryObjMetadata.cached_positionsから取得。
gap zeroing: チャンク間のギャップ位置(連続しないstart/endの隙間)をゼロ埋め。
非同期プリフェッチの全体フロー
sequenceDiagram
participant Sched as V1Impl (Scheduler)
participant LC as LookupClient
participant LS as LookupServer (Worker)
participant SM as StorageManager
participant EM as EventManager
participant Worker as V1Impl (Worker)
participant Engine as LMCacheEngine
Note over Sched: get_num_new_matched_tokens() 内
Sched->>LC: lookup(token_ids, req_id)
LC->>LS: ZMQ REQ (hashes + offsets + req_id)
LS->>SM: async_lookup_and_prefetch(lookup_id, keys, ...)
Note over SM: 各バックエンドに batched_async_contains → batched_get_non_blocking
SM->>EM: add_event(LOADING, lookup_id, all_done_task)
LS-->>LC: num_hit_tokens
LC-->>Sched: num_external_hit_tokens
Note over Sched: build_connector_meta() で LoadSpec を ConnectorMetadata に格納
Note over Worker: start_load_kv() 内
Worker->>Engine: retrieve(tokens, mask, ..., req_id=req_id)
Engine->>EM: pop_event(LOADING, req_id)
Note over Engine: future.result() で prefetch 済み MemoryObj を取得
Engine->>Engine: _async_process_tokens_internal()
Engine->>GPU: batched_to_gpu(memory_objs, ...)
ポイント:
- Scheduler側のlookupがprefetchをトリガーし、Worker側のretrieveがprefetch結果を消費する
EventManagerが両者をlookup_id(=req_id)で紐付け- prefetchは
asyncio.create_task()で非同期実行され、Worker側のretrieveまでに完了していればブロッキングなし
token_mask と ret_mask の意味
| マスク | 用途 | 値の意味 |
|---|---|---|
token_mask | adapter側で構築 | False=vLLMがキャッシュ済み(チャンク境界まで切り詰め)、True=LMCacheから要ロード |
ret_mask | Engine内部で構築 | True=実際にLMCacheから取得成功、False=未取得 |
mask(Engine引数) | token_maskと同じ | process_tokens()のFalseプレフィックス=スキップ対象 |
token_maskのFalse区間はvllm_cached_tokensをchunk_sizeの倍数に切り下げた範囲。これにより、vLLMとLMCacheのキャッシュ境界がチャンク単位で整合する(オーバーラップ領域はLMCacheが上書き)。
エラーハンドリング
- 部分的取得失敗:
ret_mask.sum() < expectedの場合、record_failed_blocks()で失敗ブロックIDを計算し、_invalid_block_idsに蓄積。vLLMのSchedulerが次stepでget_block_ids_with_load_errors()で回収し、再計算を指示 - StorageManager.batched_get():
memory_obj=Noneが返された場合、last_failed_block_start以降を切り捨て(prefix matchの性質上、途中の欠損以降は全て無効) - 健全性チェック:
is_healthy()==Falseの場合、retrieve自体をスキップ(ゼロマスクを返す)
LMCache アーキテクチャ概要
深度: [SHALLOW] 確信度: [VERIFIED] 最終更新: 2026-02-16(Phase 0a)
プロジェクト概要
LMCacheは、LLMサービングエンジン(vLLM, SGLang等)のKVキャッシュを外部に保存・再利用することで、TTFT(Time To First Token)削減とスループット向上を実現する拡張ライブラリ。
- コア機能: KVキャッシュのチャンク単位(256トークン)保存・検索・ロード
- ストレージ階層: GPU → CPU → Disk → Remote(Redis/S3/Mooncake/NIXL等)
- 統合方式: vLLMの
KVConnectorBase_V1を実装して接続 - 追加機能: CacheBlend(非プレフィックス再利用)、P2P転送、Disaggregated Prefill
リポジトリ規模
- Python: 約62,000行(
lmcache/配下) - C++/CUDA: 20ファイル(
csrc/)— CacheGen圧縮、メモリ操作、位置エンコーディングカーネル - Rust: 1ファイル(
rust/raw_block/)— Raw Block ストレージバックエンド
全体アーキテクチャ [SHALLOW]
graph TD
subgraph "サービングエンジン(vLLM)"
SE[GPUModelRunner]
end
subgraph "Integration Layer"
CONN[LMCacheConnectorV1Dynamic<br/>KVConnectorBase_V1実装]
ADAPT[LMCacheConnectorV1Impl<br/>vllm_v1_adapter.py]
end
subgraph "LMCache Core"
MGR[LMCacheManager<br/>ライフサイクル管理]
ENG[LMCacheEngine<br/>メインエンジン]
TDB[TokenDatabase<br/>トークン→チャンクキー変換]
GPU_CONN[GPUConnector<br/>GPU↔CPU転送]
SM[StorageManager<br/>ストレージ階層管理]
end
subgraph "Storage Backends"
CPU[LocalCPUBackend]
DISK[LocalDiskBackend]
RMT[RemoteBackend<br/>Redis/S3/Valkey等]
P2P[P2PBackend]
NIXL[NIXLBackend]
GDS[GdsBackend]
end
subgraph "Optional Components"
CTRL[CacheController<br/>クラスタ管理]
BLEND[CacheBlend<br/>非プレフィックス融合]
LOOKUP[LookupClient/Server<br/>キャッシュ存在確認]
MP[MultiProcess Server<br/>IPC/ZMQ]
end
SE --> CONN --> ADAPT --> MGR
MGR --> ENG
ENG --> TDB
ENG --> GPU_CONN
ENG --> SM
SM --> CPU
SM --> DISK
SM --> RMT
SM --> P2P
SM --> NIXL
SM --> GDS
ENG --> CTRL
ENG --> BLEND
MGR --> LOOKUP
パッケージ構造 [VERIFIED]
lmcache/
├── __init__.py
├── config.py # レガシー設定(旧API)
├── connections.py # 接続ヘルパー
├── logging.py # ログ初期化
├── observability.py # Prometheus/メトリクス(1,839行)
├── protocol.py # LMCacheModelRequest(msgspec)
├── usage_context.py # 利用状況トラッキング
├── utils.py # CacheEngineKey, ユーティリティ(652行)
├── non_cuda_equivalents.py # CUDA非依存フォールバック
│
├── integration/ # サービングエンジン統合
│ ├── vllm/ # vLLM統合
│ │ ├── lmcache_connector_v1.py # KVConnectorBase_V1実装(latest版)
│ │ ├── lmcache_connector_v1_085.py # vLLM 0.8.5互換版
│ │ ├── vllm_v1_adapter.py # 実装本体(LMCacheConnectorV1Impl)
│ │ ├── vllm_multi_process_adapter.py # マルチプロセス対応
│ │ └── utils.py # vLLM固有ユーティリティ
│ └── sglang/ # SGLang統合
│ ├── sglang_adapter.py
│ └── utils.py
│
├── server/ # レガシーサーバー
│
├── storage_backend/ # レガシーストレージ
│ ├── evictor/ # Eviction(LRU)
│ └── serde/ # シリアライゼーション(CacheGen等)
│
├── tools/ # ベンチマーク等
│
└── v1/ # ★ メインコード(v1アーキテクチャ)
├── cache_engine.py # LMCacheEngine(1,949行)★中核
├── cache_interface.py # LMCacheModelRequest
├── config.py # LMCacheEngineConfig
├── config_base.py # 設定基盤
├── manager.py # LMCacheManager(694行)
├── metadata.py # LMCacheMetadata
├── token_database.py # Chunked/Segment TokenDatabase
├── memory_management.py # MemoryObj, MemoryAllocator(2,339行)
├── event_manager.py # イベント管理
├── kv_layer_groups.py # レイヤーグループ管理
├── lazy_memory_allocator.py # 遅延メモリ確保
├── pin_monitor.py # ピン監視
├── periodic_thread.py # 定期実行スレッド
├── protocol.py # MPプロトコル定義
├── rpc_utils.py # ZMQ RPC
├── system_detection.py # NUMA検出
│
├── gpu_connector/ # GPU↔CPUデータ転送
│ ├── gpu_connectors.py # GPUConnectorInterface + 実装
│ ├── gpu_ops.py # CUDA memcpy操作
│ └── utils.py
│
├── storage_backend/ # ストレージ階層
│ ├── abstract_backend.py # StorageBackendInterface
│ ├── storage_manager.py # StorageManager(1,145行)
│ ├── local_cpu_backend.py # CPU (L1)
│ ├── local_disk_backend.py# Disk (L2)
│ ├── remote_backend.py # Remote (L3)
│ ├── p2p_backend.py # P2P転送
│ ├── pd_backend.py # Prefill-Decode分離
│ ├── nixl_storage_backend.py # NIXL
│ ├── gds_backend.py # GPUDirect Storage
│ ├── connector/ # 15+リモートコネクタ
│ │ ├── base_connector.py
│ │ ├── redis_connector.py
│ │ ├── s3_connector.py
│ │ ├── valkey_connector.py
│ │ ├── mooncakestore_connector.py
│ │ ├── infinistore_connector.py
│ │ ├── fs_connector.py
│ │ └── ...
│ ├── cache_policy/ # Eviction方針
│ │ ├── fifo.py / lru.py / lfu.py / mru.py
│ │ └── base_policy.py
│ ├── naive_serde/ # シリアライゼーション
│ │ ├── naive_serde.py / cachegen_*.py / kivi_serde.py
│ │ └── serde.py
│ └── job_executor/ # 非同期ジョブ実行
│
├── compute/ # 計算コンポーネント
│ ├── attention/ # Attention計算
│ │ ├── flash_attn.py / flash_infer_sparse.py
│ │ └── metadata.py
│ ├── blend/ # CacheBlend
│ │ ├── blender.py / metadata.py / utils.py
│ │ └── __init__.py
│ ├── models/ # モデル固有(Llama/Qwen3)
│ │ ├── base.py / llama.py / qwen3.py
│ │ └── utils.py
│ └── positional_encoding.py # RoPE等
│
├── multiprocess/ # マルチプロセスアーキテクチャ
│ ├── server.py # MPサーバー
│ ├── blend_server.py # CacheBlend用サーバー
│ ├── mq.py # メッセージキュー
│ ├── session.py # セッション管理
│ ├── token_hasher.py # トークンハッシュ
│ ├── gpu_context.py # GPUコンテキスト
│ ├── protocols/ # プロトコル定義
│ └── ...
│
├── cache_controller/ # クラスタキャッシュ管理
│ ├── controller_manager.py # メイン管理
│ ├── worker.py # LMCacheWorker
│ ├── executor.py # クラスタ実行
│ ├── controllers/ # KV/Registration制御
│ └── commands/ # コマンド(FullSync等)
│
├── lookup_client/ # キャッシュ存在確認
│ ├── abstract_client.py # LookupClientInterface
│ ├── lmcache_lookup_client.py # ZMQベース
│ ├── lmcache_async_lookup_client.py
│ └── record_strategies/ # 記録戦略(BloomFilter等)
│
├── distributed/ # 分散ストレージ管理(MPモード用)
│ ├── storage_manager.py # 分散StorageManager
│ ├── l1_manager.py # L1管理
│ ├── memory_manager.py # メモリ管理
│ └── eviction_policy/ # 分散Eviction
│
├── transfer_channel/ # データ転送チャネル
│ ├── abstract.py
│ ├── nixl_channel.py
│ └── py_socket_channel.py
│
├── offload_server/ # オフロードサーバー
│ ├── abstract_server.py
│ └── zmq_server.py
│
├── health_monitor/ # ヘルスチェック
├── internal_api_server/ # 内部API(FastAPI)
├── standalone/ # スタンドアロンモード
└── plugin/ # ランタイムプラグイン
主要エントリポイント [VERIFIED]
1. vLLM統合(メインパス)
- 参照:
target/LMCache/lmcache/integration/vllm/lmcache_connector_v1.py:30(LMCacheConnectorV1Dynamic) - vLLMの
KVConnectorBase_V1を実装。内部でLMCacheConnectorV1Implに委譲 LMCacheManagerがライフサイクル全体を管理
2. LMCacheEngine(コア)
- 参照:
target/LMCache/lmcache/v1/cache_engine.py:78(LMCacheEngine) - KVキャッシュのstore/retrieve/prefetchを統合するメインクラス
TokenDatabaseでトークン列→チャンクキー変換GPUConnectorでGPU↔CPU転送StorageManagerでストレージ階層管理
3. StorageManager(ストレージ階層)
- 参照:
target/LMCache/lmcache/v1/storage_backend/storage_manager.py:1 OrderedDictでバックエンド登録順管理(L1→L2→L3)- 非同期put/getで階層間データ移動
4. MultiProcess Server
- 参照:
target/LMCache/lmcache/v1/multiprocess/server.py:1 - ZMQ IPCベースのマルチプロセスアーキテクチャ
- GPUコンテキスト管理、セッション管理
5. CacheController(クラスタ管理)
- 参照:
target/LMCache/lmcache/v1/cache_controller/controller_manager.py:1 - 複数インスタンス間のキャッシュ状態管理
- Register/Deregister/Heartbeat/P2P Lookup
6. LMCache APIサーバー
- 参照:
target/LMCache/lmcache/v1/api_server/__main__.py - スタンドアロンAPIサーバーモード
新旧アーキテクチャ [VERIFIED]
lmcache/v1/: 現行アーキテクチャ(★フォーカス対象)lmcache/server/,lmcache/storage_backend/: レガシー(旧API互換)- v1がメイン。レガシーは基本的にスキップしてよい
2つの動作モード [SHALLOW]
In-Process モード
- vLLMプロセス内で直接
LMCacheEngineを動作 LMCacheConnectorV1Dynamic→LMCacheConnectorV1Impl→LMCacheManager→LMCacheEngine
MultiProcess (MP) モード
- 別プロセスでLMCacheサーバーを起動
- ZMQ IPCで通信
multiprocess/server.pyがメイン- 分散StorageManager(
distributed/storage_manager.py)を使用
データフロー概要 [SHALLOW]
Store パス(GPU → Storage)
- vLLMのforward完了後、
wait_for_save()が呼ばれる GPUConnector.from_gpu()でGPU KVキャッシュ → CPU MemoryObjTokenDatabaseでトークン列をチャンクキーに変換StorageManagerが各バックエンドに非同期put
Retrieve パス(Storage → GPU)
- Scheduler側で
LookupClientがキャッシュ存在確認 - Worker側で
start_load_kv()が呼ばれる StorageManagerがバックエンドからMemoryObjを取得GPUConnector.to_gpu()でCPU MemoryObj → GPU KVキャッシュ
重要な設計判断 [SHALLOW]
- チャンク単位保存: 256トークン(設定可)単位でKVキャッシュを分割保存
- プレフィックスハッシュ: vLLMと同じsha256ベースのハッシュチェーンでチャンク識別
- 非同期ストレージ: put操作は非同期で実行(Futureベース)
- Eviction方針: FIFO/LRU/LFU/MRUから選択可能
- Layerwise GPUConnector: レイヤー単位でのKVキャッシュ転送をサポート
- CacheBlend: 非プレフィックス部分のKVキャッシュも再利用(セパレータベースセグメント分割)
LMCacheEngine
深度: [MEDIUM] / 確信度: [VERIFIED] 最終更新: 2026-02-16(Phase 1 セッション1)
概要
LMCacheのコアコンポーネント(1,949行)。TokenDatabase、GPUConnector、StorageManagerを統合し、 KVキャッシュのstore/retrieveオペレーションを実行する。
参照: target/LMCache/lmcache/v1/cache_engine.py
Store API(2つのエントリポイント)
store_layer() — レイヤーワイズ(主要パス)
参照: target/LMCache/lmcache/v1/cache_engine.py:528
def store_layer(
tokens: Union[Tensor, list[int]],
mask: Optional[Tensor] = None,
**kwargs, # kvcaches, slot_mapping, offset, sync, req_id
) -> Generator[None, None, None]
Generator関数。呼び出し側が各attentionレイヤー実行後にnext()で進める。
初期化フェーズ(最初のyieldまで):
TokenDatabase.process_tokens(tokens, mask)→(start, end, CacheEngineKey)のイテラブルkey.split_layers(num_layers)→LayerCacheEngineKeyのリストStorageManager.contains(keys[0])で既存チェック(layer 0のキーで判定)StorageManager.batched_allocate(shape, dtype, batch_size=num_layers)でMemoryObj確保- チャンク×レイヤー → レイヤー×チャンクに転置
GPUConnector.batched_from_gpu(memory_objs, starts, ends, ...)でGPU転送Generator生成
レイヤーループ(num_layers回yield):
yield → next(mem_obj_generator) → StorageManager.batched_put(keys[layer_id], memory_objs[layer_id])
エラーハンドリング:
batched_allocateがNone → メモリ不足、storeを中止(yieldだけ行う)is_healthy()False → 全操作スキップis_frozen()True → freeze mode、yieldだけ行う
store() — 非レイヤーワイズ
参照: target/LMCache/lmcache/v1/cache_engine.py:335
全レイヤー一括転送。GPUConnector.from_gpu()で全レイヤーをまとめてコピーし、StorageManager.batched_put()で保存。レイヤーワイズが無効の場合に使用。
主要な内部状態
| フィールド | 型 | 説明 |
|---|---|---|
token_database | ChunkedTokenDatabase | トークン→チャンクハッシュ変換 |
gpu_connector | GPUConnectorInterface | GPU↔CPU転送 |
storage_manager | StorageManager | 多段バックエンド管理 |
num_layers | int | モデルのレイヤー数 |
metadata | LMCacheMetadata | model_name, world_size等 |
fmt | MemoryFormat | KV_T2D or KV_2LTD |
kv_events | list | BlockStored等のイベントキュー |
Retrieve API(2つのエントリポイント)
retrieve() — Bulk(デフォルト)
参照: target/LMCache/lmcache/v1/cache_engine.py:708
def retrieve(
tokens: Union[Tensor, list[int]],
mask: Optional[Tensor] = None,
**kwargs, # kvcaches, slot_mapping, request_configs, req_id
) -> torch.Tensor # ret_mask (bool, CPU)
全レイヤーのKVキャッシュを一括取得し、GPUのページドメモリに書き戻す。
処理フロー:
_process_tokens_internal()(同期)or_async_process_tokens_internal()(非同期prefetch済み)でMemoryObjを取得save_only_first_rank時は_broadcast_or_receive_memory_objs()で他ランクにブロードキャストGPUConnector.batched_to_gpu(memory_objs, starts, ends, ...)で一括GPU転送memory_obj.ref_count_down()で解放remove_after_retrieve時はStorageManager.remove(key)で即座に削除
_process_tokens_internal()(同期パス):
process_tokens()でチャンク分割get_block_mapping()でチャンクの所在バックエンドをprefix matchで特定batched_get(keys, location)でバックエンドからMemoryObj取得- 取得失敗時は
last_failed_block_start以降を全て無効化
_async_process_tokens_internal()(非同期パス):
event_manager.pop_event(LOADING, req_id)でprefetch済みFutureを取得future.result()でtier×chunkのMemoryObjマップを構築process_tokens()で再チャンク分割しマッチング- 未使用MemoryObjは即座に
ref_count_down()
retrieve_layer() — Layerwise
参照: target/LMCache/lmcache/v1/cache_engine.py:851
def retrieve_layer(
tokens: Union[Tensor, list[int]],
mask: Optional[Tensor] = None,
**kwargs, # kvcaches, slot_mapping, sync
) -> Generator[Optional[Tensor], None, None]
レイヤー単位でKVキャッシュを取得するGenerator関数。
初期化フェーズ:
process_tokens()でチャンク分割StorageManager.contains(layer0_key)でヒット+location統一チェック- キーをlayer-major形式に転置:
keys[chunk][layer]→keys_layer_major[layer][chunk] StorageManager.layerwise_batched_get(keys_layer_major, location)→get_generatorGPUConnector.batched_to_gpu(starts, ends, ...)→mem_obj_consumerGenerator
レイヤーループ:
yield → task = next(get_generator) → mem_objs = task.result() → mem_obj_consumer.send(mem_objs)
最終yield時にret_maskを返す。ref_count_down()は全レイヤー完了後にバッチ実行。
lookup() — ヒット数問い合わせ
参照: target/LMCache/lmcache/v1/cache_engine.py:992
Scheduler側から呼ばれるヒット数チェック。process_tokens()でチャンク分割し、StorageManagerのcontains()/batched_contains()でプレフィックスマッチ。
上流・下流
- 上流: LMCacheConnectorV1Impl(store_layer/retrieve呼び出し)
- 下流: TokenDatabase、GPUConnector、StorageManager
- ライフサイクル: LMCacheManagerが生成・管理
GPUConnector
深度: [MEDIUM] / 確信度: [VERIFIED] 最終更新: 2026-02-16(Phase 1 セッション1)
概要
GPU上のKVキャッシュとCPU上のMemoryObj間でデータを転送するコンポーネント。 vLLMのページドメモリレイアウトからslot_mappingを使って正しいデータを抽出する。
参照: target/LMCache/lmcache/v1/gpu_connector/gpu_connectors.py
クラス階層
GPUConnectorInterface (ABC)
├── VLLMPagedMemGPUConnectorV2 ← 非レイヤーワイズ(全レイヤー一括)
├── VLLMPagedMemLayerwiseGPUConnector ← レイヤーワイズ(主要パス)
├── VLLMBufferLayerwiseGPUConnector ← CacheBlend用(中間バッファ経由)
├── VLLMGPUConnectorXPU ← Intel XPU用
└── SGLangGPUConnector ← SGLang用
主要メソッド(Store方向)
batched_from_gpu()(VLLMPagedMemLayerwiseGPUConnector)
参照: target/LMCache/lmcache/v1/gpu_connector/gpu_connectors.py:1212
def batched_from_gpu(
memory_objs: List[List[MemoryObj]], # [num_layers][num_chunks]
starts: List[int],
ends: List[int],
**kwargs, # slot_mapping, sync, kvcaches
) -> Generator
Generator関数。num_layers + 1回yield。
セットアップフェーズ:
- slot_mapping_chunksを結合して
slot_mapping_fullを構築 use_gpu=True時:gpu_buffer_allocatorから中間GPUバッファを確保
レイヤーループ(各yield間):
| ステップ | use_gpu=True | use_gpu=False |
|---|---|---|
| 1 | lmc_ops.single_layer_kv_transfer()paged GPU → 中間GPUバッファ | lmc_ops.single_layer_kv_transfer()paged GPU → 直接pinned CPU |
| 2 | memory_obj.tensor.copy_(..., non_blocking=True)GPUバッファ → pinned CPU | (不要) |
lmc_ops.single_layer_kv_transfer引数:
lmc_ops.single_layer_kv_transfer(
dst_tensor, # 出力先
kvcaches[layer_id], # vLLMのページドKVキャッシュ(1レイヤー分)
slot_mapping, # トークン位置→flat slot
True, # store=True(GPU→dst方向)
True, # token_major=True(KV_T2D形式)
vllm_two_major, # vLLMの2-major形式フラグ
use_mla, # MLA形式フラグ
)
CUDAストリーム設計:
self.store_stream: 専用CUDAストリーム。メイン計算ストリームとオーバーラップ可能store_stream.wait_stream(current_stream): 計算が完了してからDMA開始sync=True時のみstore_stream.synchronize()で同期(最初のリクエストのみ)
出力形式:
- 標準:
MemoryFormat.KV_T2D=[num_tokens, 2, hidden_dim] - MLA:
MemoryFormat.KV_MLA_FMT=[num_tokens, hidden_dim]
get_shape()
参照: target/LMCache/lmcache/v1/gpu_connector/gpu_connectors.py:1331
def get_shape(num_tokens: int) -> torch.Size:
# 標準: [num_tokens, 2, hidden_dim_size]
# MLA: [num_tokens, hidden_dim_size]
主要メソッド(Retrieve方向)
batched_to_gpu()(VLLMPagedMemGPUConnectorV2 — Bulk)
参照: target/LMCache/lmcache/v1/gpu_connector/gpu_connectors.py:359
def batched_to_gpu(memory_objs, starts, ends, **kwargs)
load_stream上で全チャンクのto_gpu()を順次実行し、最後にload_stream.synchronize()。
to_gpu()はlmc_ops.multi_layer_kv_transfer()でMemoryObj → pagedメモリに全レイヤー一括転送。
batched_to_gpu()(VLLMBufferLayerwiseGPUConnector — Layerwise)
参照: target/LMCache/lmcache/v1/gpu_connector/gpu_connectors.py:683
Generator関数。num_layers + 2回イテレーションの3段パイプライン:
| 段 | 操作 | ストリーム |
|---|---|---|
| Load | CPU pinned → GPUバッファにcopy_(non_blocking=True) | load_stream |
| Compute | RoPE位置補正 + gap zeroing | default stream |
| Write | lmc_ops.single_layer_kv_transfer()でバッファ→pagedメモリ | default stream |
ダブルバッファ: compute_gpu_buffer_objとload_gpu_buffer_objをping-pongし、DMAとRoPE計算をオーバーラップ。
RoPE位置補正(cache_positions=True時):
MemoryObjMetadata.cached_positionsから保存時位置を取得fused_rotary_emb(old_positions, new_positions, K_tensor)で差分補正- これにより異なるコンテキスト位置でもKVキャッシュを再利用可能
gap zeroing: 連続しないチャンク間のギャップ位置をゼロ埋め。
mem_obj_consumer.send()パターン: Engine側からsend(mem_objs_layer)でデータを受け取る(Generator.send)。yieldでデータ受領→次イテレーションで処理。
上流・下流
- 上流: LMCacheEngine(store_layer/store/retrieve等で呼び出し)
- 下流: vLLMのページドKVキャッシュ(
self.kvcaches)、lmc_ops CUDAカーネル - 依存: lmcache.c_ops(single_layer_kv_transfer / multi_layer_kv_transfer)
設計上の注意点
- 中間GPUバッファ(
use_gpu=True)は全チャンクを結合してから一括転送するため、チャンクごとのカーネル起動オーバーヘッドを削減 kvcachesはvLLMのkv_cacheリスト(list[Tensor]、レイヤーごとに1テンソル)をinitialize_kvcaches_ptr()で受け取る- GPUバッファは
gpu_buffer_allocatorから確保し、使用後にref_count_down()で解放 - Retrieve時のCUDAカーネル: Bulk=
multi_layer_kv_transfer(全レイヤー一括)、Layerwise=single_layer_kv_transfer(store=Falseで逆方向=メモリ→paged)
StorageManager + LocalCPUBackend
深度: [DEEP] / 確信度: [VERIFIED] 最終更新: 2026-02-16(Phase 2 セッション1)
概要
多段ストレージバックエンドを管理するディスパッチャ(StorageManager)と、 L1 CPUメモリキャッシュの実装(LocalCPUBackend)。
参照:
target/LMCache/lmcache/v1/storage_backend/storage_manager.py(StorageManager)target/LMCache/lmcache/v1/storage_backend/local_cpu_backend.py(LocalCPUBackend)target/LMCache/lmcache/v1/storage_backend/abstract_backend.py(インターフェース定義)
サブドキュメント:
- memory-allocator.md — メモリアロケータ階層と物理メモリ管理
- cache-policy.md — Eviction戦略(FIFO/LRU/LFU/MRU)
- local-disk-backend.md — L2ディスクバックエンドと階層化動作
StorageManager
バックエンド登録と優先度
storage_backendsはOrderedDictで登録順=優先度:
LocalCPUBackend (L1) → LocalDiskBackend (L2) → RemoteBackend (L3)
allocator_backend: メモリ確保の責務を持つバックエンド。通常はLocalCPUBackend(PD有効時はPDBackend)。
全バックエンドはget_allocator_backend()で自身のallocator元を返す:
- LocalCPUBackend → 自身(
AllocatorBackendInterface実装) - LocalDiskBackend →
local_cpu_backend参照(CPU上に確保してからディスクに書く) - RemoteBackend →
local_cpu_backend参照
参照: target/LMCache/lmcache/v1/storage_backend/storage_manager.py:321
batched_allocate()
参照: target/LMCache/lmcache/v1/storage_backend/storage_manager.py:352
def batched_allocate(
shapes: Union[torch.Size, list[torch.Size]],
dtypes: Union[torch.dtype, list[torch.dtype]],
batch_size: int, # = num_layers
fmt: MemoryFormat = KV_2LTD,
eviction: bool = True,
busy_loop: bool = True,
) -> Optional[list[MemoryObj]]
allocator_backendに委譲。LocalCPUBackendが内部でEviction→再確保のループを行う。
batched_put()
参照: target/LMCache/lmcache/v1/storage_backend/storage_manager.py:388
処理フロー:
allocator_backendのデータをそのまま利用(コピー不要)OrderedDict順に全バックエンド(L1→L2→L3)を走査- 異なるallocatorを持つバックエンドには
allocate_and_copy_objects()で新メモリ確保+コピー- 実際にはLocalDiskBackendもRemoteBackendも
get_allocator_backend()→LocalCPUBackendなので、同一allocator=コピー不要
- 実際にはLocalDiskBackendもRemoteBackendも
- 各バックエンドの
batched_submit_put_task()を呼び出し - 全バックエンド処理後、各obj_dictの
ref_count_down()で解放
注意: put()は非推奨(RuntimeErrorを投げる)。batched_put()が唯一のエントリポイント。
運用機能
- freeze mode:
_freeze=Trueでリモートバックエンドをスキップ(LocalCPUのみ使用) - bypass mode: ヘルスチェック失敗時に特定バックエンドを一時的にバイパス
- internal_copy_stream: put時の異なるallocator間コピー用CUDAストリーム
LocalCPUBackend
AllocatorBackendInterfaceを実装。メモリ確保とキャッシュストレージの2つの役割を持つ。
submit_put_task()
参照: target/LMCache/lmcache/v1/storage_backend/local_cpu_backend.py:141
同期実行(バックグラウンドスレッドなし)。cpu_lock下で:
- 重複チェック:
key in hot_cache→ スキップ memory_obj.ref_count_up()hot_cache[key] = memory_objcache_policy.update_on_put(key)— Evictionポリシー更新batched_msg_sender.add_kv_op(ADMIT, key.chunk_hash)— controller通知(オプション)- ロック外でon_complete_callback実行
allocate() / batched_allocate() — Evictionループ
参照: target/LMCache/lmcache/v1/storage_backend/local_cpu_backend.py:426
memory_allocatorに確保試行
↓ 失敗
cache_policy.get_evict_candidates(hot_cache, num_candidates=1)
↓ 候補あり
batched_remove(evict_keys) ← hot_cacheから除去 + ref_count_down → allocatorに返却
↓
memory_allocatorに再確保試行
↓ 失敗 && busy_loop=True
0.1秒待機して再試行(他のstore完了によるメモリ解放を待つ)
batched_allocateの特殊処理: Layerwise時、1チャンクの全レイヤーをまとめて追い出す
(evict_key.split_layers(batch_size)で全レイヤーキーを生成→一括free)。
busy_loopの用途:
- store(書き込み):
busy_loop=False— 並行storeがデッドロックするため - retrieve(読み出し):
busy_loop=True— storeの完了でメモリが解放されるのを待つ
hot_cache
cache_policy.init_mutable_mapping()が返すマッピング:
- FIFO:
dict(Python dictは挿入順を保持) - LRU/MRU:
OrderedDict - LFU:
dict(freq_to_keysで別途管理)
touch_cache()
参照: target/LMCache/lmcache/v1/storage_backend/local_cpu_backend.py:128
keys_in_requestを逆順にupdate_on_hit()。suffix→prefix順にlookupされたキーを、
prefix→suffix順(正しい時系列順)に修正してアクセス順序を更新。
contains() with pin
lookup時にpin=Trueで呼ばれると:
hot_cache[key].pin()→ Eviction対象外にマークkeys_in_requestに追加 → retrieve完了後にtouch_cache()で解除
initialize_allocator()
参照: target/LMCache/lmcache/v1/storage_backend/local_cpu_backend.py:346
設定に応じてアロケータを選択:
- P2P有効時:
PagedCpuGpuMemoryAllocator(NIXL連携用ページアロケータ) - 通常時:
MixedMemoryAllocator(テンソル用PinMemory + バイナリ用BufferAllocator) - NUMA対応: GPU→NUMAマッピングでNUMA-awareなpinned memory確保
- MLA first rank: 最初のrankのみ大容量CPU確保
- reserve_cpu_size: システム利用可能メモリから予約サイズを差し引き
StorageManager(Retrieve方向)
batched_get()
参照: target/LMCache/lmcache/v1/storage_backend/storage_manager.py:484
指定locationのバックエンドからbatched_get_blocking(keys)でMemoryObjを取得。
write-back: リモートバックエンドから取得した場合、LocalCPUBackendが存在すれば自動的にL1にコピー。
layerwise_batched_get()
レイヤー単位で非同期取得。各レイヤーのbatched_get_non_blocking()をasyncio.create_taskで投入し、Futureをyield。
get_block_mapping()
チャンクリストを受け取り、各チャンクの所在バックエンドを特定。prefix match方式: 各バックエンドのbatched_contains()で先頭からの連続ヒット数を取得し、残りを次のバックエンドに渡す。
async_lookup_and_prefetch()
非同期プリフェッチの中核。LookupServerから呼ばれ、全バックエンドに対してprefix match方式でbatched_async_contains()→batched_get_non_blocking()を実行。結果はEventManagerにFutureとして登録。
バックエンドインターフェース階層
StorageBackendInterface (abstract)
├── AllocatorBackendInterface (abstract) — メモリ確保能力あり
│ └── LocalCPUBackend (concrete)
├── StoragePluginInterface (abstract) — 独自バックエンド実装用
│ └── (ユーザー定義バックエンド)
├── LocalDiskBackend (concrete)
└── RemoteBackend (concrete)
独自バックエンド実装の詳細は local-disk-backend.md 末尾の「独自バックエンド実装ガイド」を参照。
上流・下流
- 上流: LMCacheEngine(batched_allocate/batched_put/contains等)
- 下流:
- LocalCPUBackend(L1 CPUメモリ)— memory-allocator.md, cache-policy.md
- LocalDiskBackend(L2 ディスク)— local-disk-backend.md
- RemoteBackend(L3 リモート、Redis/S3等)
- 依存: CachePolicy(Eviction戦略)、MemoryAllocator(メモリプール)、EventManager(非同期prefetch)
CachePolicy — Eviction戦略
深度: [DEEP] / 確信度: [VERIFIED] 最終更新: 2026-02-16(Phase 2 セッション1)
概要
キャッシュのEviction(追い出し)戦略を抽象化するポリシーフレームワーク。 LocalCPUBackendとLocalDiskBackendが独立したCachePolicyインスタンスを持つ。
参照: target/LMCache/lmcache/v1/storage_backend/cache_policy/
BaseCachePolicy インターフェース
参照: target/LMCache/lmcache/v1/storage_backend/cache_policy/base_policy.py
class BaseCachePolicy(Generic[KeyType, MapType]):
def init_mutable_mapping(self) -> MapType
def update_on_hit(self, key, cache_dict) -> None
def update_on_put(self, key) -> None
def update_on_force_evict(self, key) -> None
def get_evict_candidates(self, cache_dict, num_candidates=1) -> list[KeyType]
重要な設計判断:
init_mutable_mapping()がhot_cache/dictの型を決定(dict, OrderedDict等)get_evict_candidates()はbest effort:can_evictチェックでpinned/参照中のオブジェクトをスキップcache_dictの値がMemoryObj(hot_cache)またはDiskCacheMetadata(disk dict)
FIFO — First In, First Out
参照: target/LMCache/lmcache/v1/storage_backend/cache_policy/fifo.py
| メソッド | 実装 |
|---|---|
init_mutable_mapping() | dict(Python dictは挿入順保持) |
update_on_hit() | 何もしない(FIFOはアクセスで順序不変) |
update_on_put() | 何もしない(dictの末尾に自然追加) |
update_on_force_evict() | 何もしない |
get_evict_candidates() | dict先頭からイテレート、can_evictなものを返す |
最もシンプル。追い出し候補の選定はO(k)(kは先頭のnon-evictableエントリ数)。
LRU — Least Recently Used
参照: target/LMCache/lmcache/v1/storage_backend/cache_policy/lru.py
| メソッド | 実装 |
|---|---|
init_mutable_mapping() | OrderedDict |
update_on_hit() | cache_dict.move_to_end(key) + chunk再利用時間追跡 |
update_on_put() | chunk初回タイムスタンプ記録 |
update_on_force_evict() | 何もしない |
get_evict_candidates() | OrderedDict先頭(最も古いアクセス)からイテレート |
追加機能: chunk_hash_to_init_timestampでチャンクの再利用間隔をPrometheusメトリクスに報告。
メモリ上限 max_num_chunk_hash=12,500,000、超過時は辞書をclear()。
LFU — Least Frequently Used
参照: target/LMCache/lmcache/v1/storage_backend/cache_policy/lfu.py
| メソッド | 実装 |
|---|---|
init_mutable_mapping() | dict |
update_on_hit() | freq++。freq_to_keys[old_freq]→freq_to_keys[new_freq]に移動 |
update_on_put() | key_to_freq[key] = 1、freq_to_keys[1]に登録 |
update_on_force_evict() | key_to_freq/freq_to_keys両方からkey除去 |
get_evict_candidates() | 最低freq→高freqの順にイテレート、同freq内はFIFO |
データ構造:
freq_to_keys:SortedDict[int, dict[key, None]]— freq順ソートkey_to_freq:dict[key, int]— O(1)でfreq逆引き- 計算量: update_on_hit = O(log N)(SortedDictの操作)
注意: get_evict_candidates()内でkey_to_freq.pop()を直接実行(副作用あり)。
MRU — Most Recently Used
参照: target/LMCache/lmcache/v1/storage_backend/cache_policy/mru.py
| メソッド | 実装 |
|---|---|
init_mutable_mapping() | OrderedDict |
update_on_hit() | cache_dict.move_to_end(key, last=True) |
update_on_put() | 何もしない |
update_on_force_evict() | 何もしない |
get_evict_candidates() | reversed(cache_dict.items())でOrderedDict末尾(最新アクセス)から |
LRUの逆。ストリーミング的アクセスパターン(同じチャンクが二度使われない場合)に有効。
Eviction発動フロー
graph TD
A[allocate() 失敗] --> B{use_hot?}
B -->|Yes| C[cache_policy.get_evict_candidates\nhot_cache, num=1]
C --> D{候補あり?}
D -->|Yes| E[batched_remove\nevict_keys]
E --> F[allocate再試行]
F --> G{成功?}
G -->|No| C
D -->|No| H{busy_loop?}
H -->|Yes| I[0.1秒待機]
I --> C
H -->|No| J[None返却]
G -->|Yes| K[MemoryObj返却]
B -->|No| H
can_evict条件: not is_pinned and ref_count == 1
- pinned: lookup中のチャンク(retrieve完了まで保護)
- ref_count > 1: 他のバックエンドやGPU転送が参照中
設定
LMCacheEngineConfig.cache_policyで指定:
cache_policy: "lru" # fifo | lru | lfu | mru
get_cache_policy()ファクトリ関数で対応するポリシーインスタンスを生成。
LocalDiskBackend — L2ディスクバックエンド
深度: [DEEP] / 確信度: [VERIFIED] 最終更新: 2026-02-16(Phase 2 セッション1)
概要
ローカルディスクにKVキャッシュを永続化するL2バックエンド。
CPU上のMemoryObjをバイト列としてファイルに書き出し/読み出しする。
メモリ確保はlocal_cpu_backendに委譲(自身はAllocatorBackendInterfaceではない)。
参照: target/LMCache/lmcache/v1/storage_backend/local_disk_backend.py
アーキテクチャ
StorageManager
├── LocalCPUBackend (L1) ← hot_cache + MemoryAllocator
├── LocalDiskBackend (L2) ← ファイル永続化 + local_cpu_backendからメモリ借用
└── RemoteBackend (L3) ← ネットワーク永続化
LocalDiskBackendはStorageBackendInterfaceを直接実装(AllocatorBackendInterfaceではない)。
get_allocator_backend() → self.local_cpu_backendを返す。
Store方向
submit_put_task()
参照: target/LMCache/lmcache/v1/storage_backend/local_disk_backend.py:291
- 重複チェック:
exists_in_put_tasks(key)→ スキップ disk_worker.insert_put_task(key)— 進行中タスクリストに登録- ディスク容量Eviction:
disk_lock下でcurrent_cache_size + required_size > max_cache_sizeの間、cache_policy.get_evict_candidates()→batched_remove()+os.remove(path) memory_obj.ref_count_up()— 非同期書き込み中の保護asyncio.run_coroutine_threadsafe()でasync_save_bytes_to_disk()を投入
async_save_bytes_to_disk()
参照: target/LMCache/lmcache/v1/storage_backend/local_disk_backend.py:479
AsyncPQThreadPoolExecutor上で実行(max_workers=4のスレッドプール)。
memory_obj.byte_array→write_file(buffer, path)memory_obj.ref_count_down()— 参照解放insert_key(key, size, shape, dtype, fmt)—self.dictにDiskCacheMetadata登録disk_worker.remove_put_task(key)on_complete_callback(key)実行(ロック外)
write_file()
- 通常:
open(path, "wb").write(buffer) - O_DIRECT: サイズがディスクブロックサイズの倍数の場合、
os.O_DIRECTフラグで直接I/O
DiskCacheMetadata
DiskCacheMetadata(path, size, shape, dtype, cached_positions, fmt, pin_count)
MemoryObjとは異なり、ディスク上のファイルパスとメタデータのみ保持。
Retrieve方向
get_blocking()
参照: target/LMCache/lmcache/v1/storage_backend/local_disk_backend.py:380
disk_lock下でself.dict[key]からpath/shape/dtype/fmt取得cache_policy.update_on_hit(key, self.dict)— アクセス順更新local_cpu_backend.allocate(shape, dtype, fmt)— CPUメモリに確保read_file(key, buffer, path)— ファイルからバッファに読み込みmetadata.cached_positionsをDiskCacheMetadataから復元
batched_get_non_blocking() — 非同期プリフェッチ
参照: target/LMCache/lmcache/v1/storage_backend/local_disk_backend.py:410
- 各キーについて:
local_cpu_backend.allocate()でCPUメモリ確保self.dict[key].pin()— 読み込み中のEviction防止cache_policy.update_on_hit()— アクセス順更新
disk_worker.submit_task("prefetch", batched_async_load_bytes_from_disk, ...)- priority=0(最高優先度)でスレッドプールに投入
batched_async_load_bytes_from_disk()
各ファイルをread_file()で読み込み後、self.dict[key].unpin()。
read_file()
- 通常:
open(path, "rb").readinto(buffer) - O_DIRECT: ブロック整列時のみ
os.O_DIRECT | os.O_RDONLY - FileNotFoundError時: 警告ログ + dictからキー除去
LocalDiskWorker — 優先度付きスレッドプール
参照: target/LMCache/lmcache/v1/storage_backend/local_disk_backend.py:37
| タスク種別 | 優先度 | 説明 |
|---|---|---|
| prefetch | 0 (最高) | ディスク→CPUメモリ読み込み |
| delete | 1 | ファイル削除 |
| put | 2 (最低) | CPUメモリ→ディスク書き込み |
AsyncPQThreadPoolExecutor(Priority Queue付き非同期スレッドプール)で管理。
prefetchが最優先なのは、retrieve(推論の待ち時間に直結)をstoreより優先するため。
容量管理
max_cache_size:config.max_local_disk_size * 1024^3(バイト単位)current_cache_size: 現在のディスク使用量(書き込み時に加算、削除時に減算)- Eviction: store時にmax超過なら
cache_policy.get_evict_candidates()→os.remove()
注意: ディスクフラグメンテーションは未考慮(TODO)。
独自ストレージバックエンド実装ガイド
StoragePluginInterface
参照: target/LMCache/lmcache/v1/storage_backend/abstract_backend.py:394
独自バックエンドを実装する場合はStoragePluginInterfaceを継承する:
class MyBackend(StoragePluginInterface):
def __init__(self, dst_device, config, metadata, local_cpu_backend, loop):
super().__init__(dst_device, config, metadata, local_cpu_backend, loop)
必須メソッド(StorageBackendInterface由来)
| メソッド | 役割 |
|---|---|
contains(key, pin) | キーの存在確認。pin=Trueで追い出し保護 |
exists_in_put_tasks(key) | 書き込み進行中のキー確認(重複store防止) |
batched_submit_put_task(keys, objs, ...) | 非同期書き込み投入 |
get_blocking(key) | 同期的なKVデータ取得 |
pin(key) / unpin(key) | Eviction保護の制御 |
remove(key, force) | エントリ削除 |
get_allocator_backend() | メモリ確保先(通常local_cpu_backendを返す) |
close() | リソース解放 |
オプショナルメソッド
| メソッド | デフォルト動作 | オーバーライド推奨 |
|---|---|---|
batched_get_blocking(keys) | 個別get_blocking()のループ | バッチ取得が効率的な場合 |
batched_async_contains(lookup_id, keys, pin) | NotImplementedError | 非同期prefetch対応時 |
batched_get_non_blocking(lookup_id, keys, ...) | NotImplementedError | 非同期prefetch対応時 |
batched_contains(keys, pin) | 個別contains()のループ(prefix match方式) | バッチ判定が効率的な場合 |
touch_cache() | (定義なし) | LRU等のアクセス順更新が必要な場合 |
メモリ確保パターン
独自バックエンドからデータを取得する場合:
local_cpu_backend.allocate(shape, dtype, fmt)でCPUメモリを確保- データをMemoryObjの
byte_arrayに書き込み(readinto()等) metadata.cached_positions等のメタデータを復元- MemoryObjを返却(呼び出し元がref_count管理)
on_complete_callback
batched_submit_put_task()のオプションパラメータ。
各キーのstore完了時にコールバックを実行(バッチ単位ではなくキー単位)。
他バックエンドへの連鎖store等に使用可能。実装側でcatch/logすること。
メモリアロケータ階層
深度: [DEEP] / 確信度: [VERIFIED] 最終更新: 2026-02-16(Phase 2 セッション1)
概要
LMCacheのメモリ管理は、事前確保された大きなバッファ上で仮想アドレス空間管理を行う カスタムアロケータで実現されている。pinned CPUメモリ上に確保することで、 GPU⇔CPU間のDMA転送を高速化する。
参照: target/LMCache/lmcache/v1/memory_management.py
アロケータ階層
MemoryAllocatorInterface (abstract)
├── TensorMemoryAllocator ← explicit free list方式
├── PagedTensorMemoryAllocator ← ページ単位の固定サイズスロット
├── BufferAllocator ← バイト配列用(GC任せ)
├── HostMemoryAllocator ← 非pinned CPU + 内部委譲
├── PinMemoryAllocator ← pinned CPU + 内部委譲
├── MixedMemoryAllocator ← テンソル用Pin + バイナリ用Buffer
├── GPUMemoryAllocator ← GPU VRAM + 内部委譲
├── CuFileMemoryAllocator ← GPUDirect Storage対応
├── PagedCpuGpuMemoryAllocator ← CPU+GPU両方のページアロケータ(NIXL/P2P用)
└── AdHocMemoryAllocator ← テスト用ダミー
TensorMemoryAllocator — explicit free list方式
参照: target/LMCache/lmcache/v1/memory_management.py:1135
事前確保されたフラットテンソル(buffer)上で、AddressManagerが仮想アドレス空間を管理。
AddressManager
参照: target/LMCache/lmcache/v1/memory_management.py:903
- データ構造:
SortedList[FreeBlock](sortedcontainersライブラリ) - FreeBlock:
(start, size)タプル。startでソート - アライメント: デフォルト4,096バイト(
ALIGN_BYTES) - 確保: first-fit方式。ソートリストを先頭から走査し最初に十分な空きブロックを選択
- 解放: bisect_leftで挿入位置を特定し、前後のブロックとcoalesce(結合)
- sbrk(): アドレス空間の動的拡張(LazyAllocator向け)
- スレッドセーフ:
@synchronized("_lock")デコレータで全操作をロック保護
allocate() の動作
aligned_size = (raw_size + 4095) & ~4095 # 4KB境界に切り上げ
# SortedListからfirst-fitで空きブロックを探索
# ブロックを分割: [確保分 | 残余→フリーリストに戻す]
# buffer[start:start+raw_size] をraw_dataとしてTensorMemoryObjを生成
batched_allocate() の最適化
通常のallocateはブロックごとに個別確保だが、batched_allocateは
unit_aligned_size * batch_size の1つの大きなブロックとして確保し、
torch.chunk()で分割。これによりフラグメンテーションを軽減。
free() / batched_free()
free(): AddressManager.free()でブロックを返却→前後のFreeBlockとcoalescebatched_free(): メモリブロックをアドレス順にソートし、隣接ブロックを事前にcoalesceしてから AddressManager.free()を呼ぶ。フリーリスト操作回数を削減
PagedTensorMemoryAllocator — ページスロット方式
参照: target/LMCache/lmcache/v1/memory_management.py:1404
固定サイズのスロット(ページ)に分割し、deque[TensorMemoryObj]でフリーリストを管理。
- 初期化:
bufferをalign_bytes(=1チャンク分のKVデータサイズ)で分割 - allocate():
free_blocks.popleft()— O(1) - free():
free_blocks.append(mem_obj)— O(1)、invalidate()しない(再利用) - スレッドセーフ:
dequeのCPython実装がアトミックなため、ロック不要 - 用途: P2P/PD共有時のNIXL連携(ページ単位のアドレス管理が必要)
MixedMemoryAllocator — 通常時のデフォルト
参照: target/LMCache/lmcache/v1/memory_management.py:1892
LocalCPUBackendのinitialize_allocator()がデフォルトで生成するアロケータ。
MixedMemoryAllocator
├── pin_allocator: TensorMemoryAllocator or PagedTensorMemoryAllocator
│ └── buffer: pinned CPU memory(NUMA-aware確保可能)
└── buffer_allocator: BufferAllocator
└── 個別bytearrayを都度確保
- MemoryFormat分岐: KV_2LTD/KV_T2D/KV_2TD/KV_MLA_FMT → pin_allocator、BINARY_BUFFER → buffer_allocator
- pinned memory確保:
lmc_ops.alloc_pinned_ptr()(CUDAのcudaHostAlloc相当)- NUMA対応時:
lmc_ops.alloc_pinned_numa_ptr(size, numa_id)
- NUMA対応時:
- close():
lmc_ops.free_pinned_ptr()で明示解放
MemoryObj — メモリオブジェクトの抽象
TensorMemoryObj
参照: target/LMCache/lmcache/v1/memory_management.py:431
- raw_data: フラットな
torch.Tensor(uint8ビュー)。アロケータのバッファスライス - metadata:
MemoryObjMetadata(shape, dtype, address, phy_size, ref_count, pin_count, fmt, cached_positions) - group_prefix_sum: 複数グループ(shapes/dtypes)のバイトオフセットプレフィックスサム
- tensor プロパティ:
raw_data[:logical_size].view(dtype).view(shape)でテンソルビューを返す - get_tensor(index): グループ別テンソルビュー(MLA等の複数形状対応)
ref_count / pin_count ライフサイクル
確保時: ref_count=1, pin_count=0
↓
hot_cacheに登録: ref_count_up() → ref_count=2
↓
GPU転送中: ref_count_up() → ref_count=3
↓
GPU転送完了: ref_count_down() → ref_count=2
↓
hot_cacheから追い出し: ref_count_down() → ref_count=1
↓
batched_put完了: ref_count_down() → ref_count=0 → allocator.free()
pin_count: lookup時にpin()でEviction対象外にマーク。
can_evict = not is_pinned and ref_count == 1(ref_count=1=hot_cacheのみが保持)- PinMonitor: タイムアウト追跡。異常なpin長期化を検出
BytesBufferMemoryObj
bytes/bytearrayのラッパー。ref_count操作はno-op(GC任せ)。
BINARY_BUFFER形式用。Serde圧縮結果の格納に使用。
メモリサイズ計算
calculate_chunk_budget()
参照: target/LMCache/lmcache/v1/storage_backend/local_cpu_backend.py:688
max_chunks = total_memory // aligned_chunk_bytes
非同期ローディングシステムでの同時確保数上限を算出。 デッドロック防止のためのチャンクバジェット管理に使用。
get_full_chunk_size()
Layerwise時: chunk_tokens * kv_size * hidden_dim * dtype_size(1レイヤー分)
Bulk時: kv_size * num_layers * chunk_tokens * hidden_dim * dtype_size(全レイヤー分)
TokenDatabase(ChunkedTokenDatabase)
深度: [MEDIUM] / 確信度: [VERIFIED] 最終更新: 2026-02-16(Phase 1 セッション1)
概要
トークン列をチャンクに分割し、プレフィックスチェーンハッシュを計算してCacheEngineKeyを生成する。 vLLMのプレフィックスキャッシュと同一のハッシュアルゴリズムを使用し、キーの互換性を保証する。
参照: target/LMCache/lmcache/v1/token_database.py
クラス階層
TokenDatabase (ABC)
├── ChunkedTokenDatabase ← 標準実装(固定サイズチャンク)
└── SegmentTokenDatabase ← CacheBlend用(セパレータベースの可変長チャンク)
主要メソッド
process_tokens()
参照: target/LMCache/lmcache/v1/token_database.py:309
def process_tokens(
tokens: Optional[Union[Tensor, List[int]]] = None,
hashes: Optional[List[int]] = None,
offsets: Optional[List[int]] = None,
mask: Optional[Tensor] = None,
make_key: bool = True,
request_configs: Optional[dict] = None,
) -> Iterable[ProcessTokensResult] # (start, end, CacheEngineKey|hash)
2つの入力モード:
- tokens入力: トークン列を受け取り、チャンク分割→ハッシュ計算
- hashes入力: 事前計算済みハッシュ+offsetsを受け取り、キー生成のみ
チャンク分割アルゴリズム(tokens入力時):
_chunk_tokens(): chunk_size(デフォルト256)単位で分割save_unfull_chunk=True(デフォルト): 端数チャンクも保存save_unfull_chunk=False: 端数は切り捨て
_prefix_hash(): プレフィックスチェーンハッシュを計算- 初期値:
NONE_HASH(vLLMから取得、kv_cache_utils.init_none_hash()) - 各チャンク:
hash_func((previous_hash, token_tuple, extra_keys))
- 初期値:
- maskのFalse区間(=already-cached prefix)のチャンクをスキップ
- 制約: False数はchunk_sizeの倍数でなければならない(ValueError)
ハッシュ関数
参照: target/LMCache/lmcache/v1/token_database.py:97 (_get_vllm_hash_func)
vLLMのget_hash_fn_by_name("sha256_cbor")を直接利用。
複数のインポートパスを試行し、vLLMバージョン互換性を確保:
vllm.utils.hashing.get_hash_fn_by_name(PR#27151以降)vllm.utils.get_hash_fn_by_name(PR#27151以前)sha256_cbor_64bit→sha256_cborリネーム対応(PR#23673)- フォールバック: Python組み込み
hash()(非推奨、分散キャッシュで不整合の可能性)
CacheEngineKey生成
参照: target/LMCache/lmcache/v1/token_database.py:207 (_make_key_by_hash)
CacheEngineKey(
model_name, # メタデータから
world_size, # save_only_first_rank時は1に固定
worker_id, # GPUランク
chunk_hash, # プレフィックスチェーンハッシュ
kv_dtype, # e.g. bfloat16
request_configs, # オプション(per-requestの設定dict)
)
save_only_first_rankはMLA(Multi-head Latent Attention)使用時に有効。world_sizeを1に固定することで、異なるTP並列度でもキーが一致する。
Retrieve時の利用
Retrieveパスでも同じprocess_tokens()が使用される:
- Bulk retrieve:
_process_tokens_internal()と_async_process_tokens_internal()の両方で呼ばれ、チャンクキーを生成 - Layerwise retrieve:
retrieve_layer()内で呼ばれ、layer 0のキーでStorageManager.contains()判定 - LookupClient: Schedulerプロセス内でも独自の
ChunkedTokenDatabaseインスタンスを持ち、process_tokens(make_key=False)でハッシュのみ計算してZMQ経由でLookupServerに送信 - 非同期prefetch:
async_process_tokens_internal()ではハッシュ再計算が発生する(TODO: スキップ最適化の余地あり)
上流・下流
- 上流: LMCacheEngine(store_layer/store/retrieve等で呼び出し)、LookupClient(lookup時)
- 下流: なし(純粋な変換コンポーネント)
- 依存: vLLMのハッシュ関数ライブラリ
vLLM統合(LMCacheConnector)
深度: [MEDIUM] / 確信度: [VERIFIED] 最終更新: 2026-02-16(Phase 1 セッション1)
概要
LMCacheとvLLMを接続するアダプタ層。vLLMのKVConnectorBase_V1インターフェースを実装し、
attentionレイヤー実行中のKVキャッシュstore/retrieveをLMCacheに委譲する。
クラス階層
KVConnectorBase_V1 (vLLM)
└── LMCacheConnectorV1Dynamic ← vLLMに登録される外殻
└── LMCacheConnectorV1Impl ← 実装本体(Worker側)
└── LMCacheEngine ← 直接参照(LMCacheManager経由で取得)
参照:
target/LMCache/lmcache/integration/vllm/lmcache_connector_v1.py(Dynamic)target/LMCache/lmcache/integration/vllm/vllm_v1_adapter.py:964(Impl.save_kv_layer)
主要メソッド(Store方向)
save_kv_layer()
シグネチャ: save_kv_layer(layer_name: str, kv_layer: Tensor, attn_metadata: AttentionMetadata, **kwargs)
vLLMの各attentionレイヤー実行後に呼ばれる。
use_layerwise=Falseの場合は即座にreturn(非レイヤーワイズパスは別経路)kv_role="kv_consumer"の場合もreturn(consumeのみ、storeしない)
Layer 0の処理:
connector_metadata.requestsからsave_spec.can_save=Trueのリクエストを抽出skip_leading_tokensをchunk_size(256)の倍数に切り下げてマスク整合store_maskを構築:prefix部分=False、新規部分=TrueLMCacheEngine.store_layer()でGenerator生成、self.layerwise_storersに追加- 最初のリクエストのみ
sync=True(CUDAストリーム同期)
全レイヤー共通: layerwise_storers内の全Generatorをnext()で1ステップ進行。
ConnectorMetadata
参照: target/LMCache/lmcache/integration/vllm/vllm_v1_adapter.py (LMCacheConnectorMetadata)
Scheduler側で構築され、Worker側に渡される。各リクエストのtoken_ids、slot_mapping、save_spec(can_save, skip_leading_tokens)を含む。
上流・下流
- 上流: vLLM GPUModelRunner(
save_kv_layerフック) - 下流: LMCacheEngine(
store_layer/store/retrieve/retrieve_layer) - 関連: LMCacheManager(ライフサイクル管理、Engine取得)
主要メソッド(Retrieve方向)
Scheduler側: get_num_new_matched_tokens()
参照: target/LMCache/lmcache/integration/vllm/vllm_v1_adapter.py:1193
vLLM Schedulerのschedule()から呼ばれ、外部KVキャッシュのヒット数を返す。
LookupClient.lookup_cache(req_id)で既存キャッシュ確認(2回目以降)- 未キャッシュなら
LookupClient.lookup(token_ids, req_id)でZMQ経由でWorker側に問い合わせ LoadSpec(vllm_cached_tokens, lmcache_cached_tokens, can_load=False)を生成update_state_after_alloc()でcan_load=Trueに更新(ブロック確保成功時)build_connector_meta()でReqMeta(load_spec=LoadSpec)をLMCacheConnectorMetadataに格納
Worker側: start_load_kv()
参照: target/LMCache/lmcache/integration/vllm/vllm_v1_adapter.py:737
vLLMのforward実行前にForwardContextから呼ばれ、KVキャッシュのGPU復元を開始。
token_maskの構築:
request.load_spec.vllm_cached_tokensをchunk_sizeの倍数に切り下げ →masked_token_counttoken_mask[:masked_token_count] = False(vLLM既キャッシュ分)、残り=True
2モード分岐:
- Layerwise (
use_layerwise=True):LMCacheEngine.retrieve_layer()でGenerator取得next()× 2回で先行2レイヤー分をキックself.layerwise_retrieversにGenerator追加
- Bulk (
use_layerwise=False):LMCacheEngine.retrieve()を呼び出し、ret_maskを取得- 取得失敗時は
record_failed_blocks()で失敗ブロックIDを_invalid_block_idsに記録
Worker側: wait_for_layer_load()
参照: target/LMCache/lmcache/integration/vllm/vllm_v1_adapter.py:940
各attentionレイヤー実行前に呼ばれ、該当レイヤーのKVロード完了を待機。
layerwise_retrievers内の全Generatorをnext()で1ステップ進行。
最終レイヤーではret_maskを取得して検証。
設計上の注意点
LMCacheConnectorV1Dynamicは純粋な委譲シェル。全メソッドがself._lmcache_engine(V1Impl)に転送- LMCacheManagerにstore()メソッドは存在しない。V1ImplがEngineを直接呼ぶ
kv_roleは"kv_both"(default)/"kv_producer"/"kv_consumer"の3値。producer時はskip_leading_tokens=0(全トークンstore)current_layerカウンタでレイヤー追跡。wait_for_save()でリセット- Retrieve 2モード:
use_layerwise(デフォルトFalse)でBulk/Layerwiseを切替 - LookupClient-LookupServer分離: SchedulerプロセスのLookupClientからWorkerプロセスのLookupServerにZMQ IPC通信
- LoadSpec: Scheduler→Worker間でlookup結果を伝達するデータ構造(ConnectorMetadata経由)
CacheBlend: 非プレフィックスKVキャッシュ再利用
深度: [MEDIUM] 確信度: [VERIFIED] 最終更新: 2026-02-19(Phase 2 CacheBlend調査)
概要
CacheBlendは、プレフィックスが一致しない場合でもKVキャッシュを再利用する機能。通常のプレフィックスキャッシュはトークン列が同一プレフィックスで始まる場合のみ再利用できるが、CacheBlendはドキュメントの順序が変わっても再利用できる。
解決する問題
RAG(Retrieval Augmented Generation)などで複数ドキュメントをプロンプトに含める場合:
- 1回目:
[SYS] [SEP] [Doc A] [SEP] [Doc B] [SEP] [Doc C] [SEP] [質問1] - 2回目:
[SYS] [SEP] [Doc B] [SEP] [Doc A] [SEP] [Doc C] [SEP] [質問2]
2回目はプレフィックスが変わるため通常のキャッシュは使えないが、CacheBlendはDoc A/B/Cそれぞれの事前計算KVキャッシュを再利用してblendする。
アーキテクチャ全体図
graph TD
subgraph "vLLM(パッチ必須)"
GW[gpu_worker.py<br/>load_model()]
VMT[VLLMModelTracker<br/>register_model()]
GW --> VMT
end
subgraph "LMCacheConnectorV1Impl"
SLK[start_load_kv<br/>blender.blend()]
end
subgraph "LMCBlender"
BL[blend_layer()<br/>Generator]
PQ[process_qkv()<br/>重要token同定]
end
subgraph "LMCBaseModel(compute_layer)"
EMB[embedding]
LN[layernorm]
QKV[qkv_proj]
ROPE[rotary_emb]
ATT[flash_attn]
MLP[mlp]
end
subgraph "LMCacheEngine"
RL[retrieve_layer()<br/>Generator]
end
subgraph "Storage"
CPU[LocalCPUBackend]
DISK[LocalDiskBackend]
end
VMT --> |get_model| BL
SLK --> BL
BL --> |interleave| RL
BL --> |interleave| EMB
EMB --> LN --> QKV --> ROPE
ROPE --> PQ
PQ --> |重要tokenのみK/V更新| ATT
ATT --> MLP
RL --> CPU
CPU --> DISK
コンポーネント詳細
1. LMCBlender [VERIFIED]
参照: target/LMCache/lmcache/v1/compute/blend/blender.py:18
CacheBlendのメイン制御クラス。
class LMCBlender:
def __init__(self, cache_engine, gpu_connector, vllm_model, config):
self.layerwise_model = infer_model_from_vllm(vllm_model, self, enable_sparse)
self.num_layers = len(vllm_model.model.layers)
self.common_metadata = LMCBlendCommonMetadata(
check_layers=config.blend_check_layers,
recomp_ratios=config.blend_recompute_ratios,
thresholds=config.blend_thresholds,
)
blend_layer() — レイヤーワイズ処理のGenerator [VERIFIED]
参照: target/LMCache/lmcache/v1/compute/blend/blender.py:124
def blend_layer(self, tokens, mask=None, **kwargs):
layerwise_model_executor = self.layerwise_model.compute_layer(tokens)
layerwise_retriever = self.cache_engine.retrieve_layer(tokens, mask, **kwargs)
next(layerwise_retriever) # 初期化
yield
for i in range(self.num_layers):
next(layerwise_retriever) # レイヤーiのKVキャッシュをGPUへロード
next(layerwise_model_executor) # レイヤーiのforward計算
yield
next(layerwise_retriever) # 後処理
self.metadata.clean()
yield
ポイント: retrieve_layer(KVキャッシュロード)とcompute_layer(forward計算)が各レイヤーで同期しながら交互に進む。KVキャッシュをロードしてからforward計算に使用。
process_qkv() — 重要token同定ロジック [VERIFIED]
参照: target/LMCache/lmcache/v1/compute/blend/blender.py:59
check_layersで指定したレイヤーで重要tokenを決定する:
def process_qkv(self, q, k, v, residual, layer_id, attn_output, attn_metadata):
old_k, old_v = self.gpu_connector.get_kv(layer_id)
# RoPE位置エンコーディング適用
q, k = attn_layer.rotary_emb(self.metadata.positions, q, k)
if layer_id in self.common_metadata.check_layers:
# K差分のL2ノルム(token次元でsum)
diff_k = torch.sum((k.to(float32) - old_k.to(float32)) ** 2, dim=[1])
total_len = diff_k.shape[0]
# recomp_ratios[0]の割合のtokenをtopk選択
topk_num = int(total_len * self.common_metadata.recomp_ratios[0])
top_indices = torch.topk(diff_k, k=topk_num).indices
top_indices, _ = torch.sort(top_indices) # 順序保持
# 重要tokenのみ選択してforward継続
k, v = k[top_indices], v[top_indices]
q = q[top_indices]
self.metadata.imp_indices = top_indices
if self.metadata.imp_indices is not None:
# 重要tokenのみold_k/vを更新
old_k[self.metadata.imp_indices] = k
old_v[self.metadata.imp_indices] = v
return q, old_k, old_v, ... # 完全なK/V(重要token更新済み)
else:
return q, k, v, ...
アルゴリズム:
- キャッシュから取得した
old_kと新たに計算したkの差分L2ノルムを計算 - 差分が大きい(= キャッシュが不正確)tokenを
recomp_ratios割合だけtopk選択 - 重要tokenのみQ/K/Vを保持して再計算(他はキャッシュ値を使用)
- 最終的に完全なK/V(重要token部分は更新済み)でAttentionを計算
2. LMCBaseModel.compute_layer() — モデルforward [VERIFIED]
参照: target/LMCache/lmcache/v1/compute/models/base.py:66
@torch.compileデコレータが付いた独自forwardループ。vLLMの推論エンジンを迂回してLMCache独自の計算グラフを構築。
@torch.compile
def compute_layer(self, input_ids):
hidden_states = self.vllm_model.get_input_embeddings(input_ids)
for idx, layer in enumerate(self.vllm_model.model.layers[...]):
# QKV投影
qkv, _ = layer.self_attn.qkv_proj(hidden_states)
q, k, v = qkv.split([q_size, kv_size, kv_size], dim=-1)
# モデル固有QKV処理(GQA等)
q, k, v = self._process_qkv(q, k, v, layer)
# LMCBlenderのprocess_qkv呼び出し(重要token選択)
q, k, v, residual, attn_output, attn_metadata = \
self.blender.process_qkv(q, k, v, residual, idx, ...)
# Attention計算(重要tokenのみ)
attn_output = self.lmc_attn_layers[idx].forward_contiguous(...)
# MLP
hidden_states = layer.mlp(hidden_states)
yield # 各レイヤー処理後にyield(blend_layer()と同期)
対応モデル (3種のみ):
LlamaForCausalLM→LMCLlamaModelQwen2ForCausalLM→LMCLlamaModel(同実装)Qwen3ForCausalLM→LMCQwen3Model
参照: target/LMCache/lmcache/v1/compute/models/utils.py:14 (infer_model_from_vllm)
3. VLLMModelTracker — モデル参照管理 [VERIFIED]
参照: target/LMCache/lmcache/v1/compute/models/utils.py:38
class VLLMModelTracker:
_vllm_models: Dict[str, nn.Module] = {}
@classmethod
def register_model(cls, instance_id: str, vllm_model: nn.Module): ...
@classmethod
def get_model(cls, instance_id: str) -> nn.Module: ...
クラス変数として全インスタンスで共有するシングルトン的レジストリ。instance_idはENGINE_NAME(LMCacheの定数)が使われる。
4. LMCBlenderBuilder — ブレンダー生成 [VERIFIED]
参照: target/LMCache/lmcache/v1/compute/blend/utils.py:22
class LMCBlenderBuilder:
@classmethod
def get_or_create(cls, instance_id, cache_engine, gpu_connector, config):
if instance_id not in cls._blenders:
vllm_model = VLLMModelTracker.get_model(instance_id)
blender = LMCBlender(cache_engine, gpu_connector, vllm_model, config)
cls._blenders[instance_id] = blender
return cls._blenders[instance_id]
5. SegmentTokenDatabase — セグメント単位ハッシュ [VERIFIED]
参照: target/LMCache/lmcache/v1/token_database.py:393
CacheBlend使用時はChunkedではなくSegmentTokenDatabaseを使用。
class SegmentTokenDatabase(TokenDatabase):
def __init__(self, config, metadata):
self.tokenizer = AutoTokenizer.from_pretrained(metadata.model_name)
self.sep_tokens = tokenizer.encode(config.blend_special_str)[1:] # [1:]でBOS除去
self.sep_len = len(self.sep_tokens)
def _fast_split_by_subtensor(self, tokens):
"""スライディングウィンドウでsep_tokensを検索して分割"""
windows = tokens.unfold(0, self.sep_len, 1)
matches = (windows == self.sep_tokens).all(dim=1).nonzero(...)
# マッチ位置でtokensを分割してyield
def process_tokens(self, tokens, ...):
"""各セグメントごとに独立したハッシュを生成(プレフィックスチェーンではない)"""
for token_chunk in self._fast_split_by_subtensor(tokens):
yield (start_idx, end_idx, self._make_key_by_hash(self._hash_tokens(token_chunk)))
ChunkedTokenDatabaseとの違い:
- Chunked: 全トークンのプレフィックスハッシュチェーン(順序依存)
- Segment: セパレータで分割した各セグメントを独立ハッシュ(順序非依存)
vLLMで動かす方法
必要なパッチ(vLLM本体への変更)
参照: target/LMCache/examples/blend_kv_v1/README.md
target/vllm/vllm/v1/worker/gpu_worker.pyのload_model()末尾に追加:
# load_model()の末尾(self.model_runner.load_model()の後)
from lmcache.v1.compute.models.utils import VLLMModelTracker
from lmcache.integration.vllm.utils import ENGINE_NAME
VLLMModelTracker.register_model(ENGINE_NAME, self.model_runner.model)
ensure_kv_transfer_initialized(self.vllm_config)
なぜ必要か: LMCacheのforward計算(compute_layer)がvLLMモデルの.model.layers[]に直接アクセスするため、実行時にvLLMモデルの参照が必要。KV Transferはinitialize_from_config()で初期化されるため順序に注意。
注意: READMEでは
init_worker_distributed_environment内のensure_kv_transfer_initializedをコメントアウトと記載。ただし最新vLLMでは同関数はinitialize_from_config()内に移動済みのため、パッチ内容は使用するvLLMバージョンに依存する。
環境変数による設定
参照: target/LMCache/examples/blend_kv_v1/blend.py:20
# 基本設定
export LMCACHE_CHUNK_SIZE=256
export LMCACHE_USE_LAYERWISE=True # CacheBlendにはlayerwiseが必須
# Blending設定
export LMCACHE_ENABLE_BLENDING=True
export LMCACHE_BLEND_SPECIAL_STR=" # # " # セパレータ文字列
export LMCACHE_BLEND_CHECK_LAYERS=1 # 重要token判定レイヤー(レイヤー1で判定)
export LMCACHE_BLEND_RECOMPUTE_RATIOS=0.15 # 再計算するtoken割合(15%)
# ストレージ(CPU)
export LMCACHE_LOCAL_CPU=True
export LMCACHE_MAX_LOCAL_CPU_SIZE=5 # GB
# スパースアテンション(任意、FLASHINFERが必要)
export VLLM_ATTENTION_BACKEND=FLASHINFER
export LMCACHE_EXTRA_CONFIG='{"enable_sparse": true}'
Pythonコード
from vllm import LLM, SamplingParams
from vllm.config import KVTransferConfig
from vllm.engine.arg_utils import EngineArgs
from lmcache.v1.cache_engine import LMCacheEngineBuilder
from lmcache.integration.vllm.utils import ENGINE_NAME
ktc = KVTransferConfig(
kv_connector="LMCacheConnectorV1",
kv_role="kv_both",
)
llm_args = EngineArgs(
model="meta-llama/Llama-2-7b-chat-hf", # Llama/Qwen2/Qwen3のみ対応
kv_transfer_config=ktc,
enable_prefix_caching=False, # 必須: CacheBlendと非互換
enforce_eager=True, # 必須: CUDAGraphはCacheBlendと非互換
max_model_len=32768,
gpu_memory_utilization=0.7,
)
llm = LLM(**asdict(llm_args))
# プロンプト構築: セパレータでセグメントを区切る
sep = tokenizer.encode(" # # ")[1:] # [1:]でBOS除去
prompt = sys_tokens + sep + doc_a + sep + doc_b + sep + query_tokens
# 後処理
LMCacheEngineBuilder.destroy(ENGINE_NAME)
プロンプト設計のポイント
CacheBlendが効果を発揮するプロンプト構造:
[SYS_PROMPT] [SEP] [Document_A] [SEP] [Document_B] [SEP] [Document_C] [SEP] [QUERY]
- 各セグメントがセパレータ(
LMCACHE_BLEND_SPECIAL_STR)で区切られる - セグメントの順序が変わっても各セグメントのKVキャッシュを再利用可能
- セグメントはchunk_size(256トークン)の倍数に揃えると効率的
BlendEngine(MultiProcessモード)
参照: target/LMCache/lmcache/v1/multiprocess/blend_server.py:98
MultiProcessモードのCacheBlend用サーバー。MPCacheEngineを継承し、セパレータベースの段落分割・プリコンピュート保存・取得を提供。
class BlendEngine(MPCacheEngine):
BLEND_HASH_PREFIX = 0xB1ED # 通常キャッシュとBlendキャッシュを区別するプレフィックス
def __init__(self, sep_tokens, storage_manager_config, chunk_size=256):
super().__init__(storage_manager_config, chunk_size, hash_algorithm="blake3")
self._token_matcher = ParallelPatternMatcher(sep_tokens) # C拡張による高速マッチング
主要メソッド
| メソッド | 役割 |
|---|---|
cb_register_kv_cache | GPUバッファ(KVキャッシュ)を登録 |
cb_lookup_pre_computed | 事前計算済みチャンクのlookup(各段落ごとにprefetch) |
cb_store_pre_computed | 事前計算済みチャンクをストレージに保存(BLEND_HASH_PREFIXでハッシュ計算) |
cb_retrieve_pre_computed | ストレージからGPUバッファへKVキャッシュをコピー |
cb_store_final | 最終KVキャッシュを通常ハッシュで保存(通常モードLLMでも利用可能に) |
ハッシュ区別: BLEND_HASH_PREFIX=0xB1EDでプリコンピュートキャッシュと通常キャッシュを区別。
設定パラメータ一覧
参照: target/LMCache/lmcache/v1/config.py:100
| 環境変数 | Pythonキー | デフォルト | 説明 |
|---|---|---|---|
LMCACHE_ENABLE_BLENDING | enable_blending | False | CacheBlend有効化 |
LMCACHE_BLEND_SPECIAL_STR | blend_special_str | " # # " | セパレータ文字列 |
LMCACHE_BLEND_CHECK_LAYERS | blend_check_layers | None | 重要token判定レイヤー(カンマ区切りリスト) |
LMCACHE_BLEND_RECOMPUTE_RATIOS | blend_recompute_ratios | None | 再計算割合(カンマ区切りfloatリスト) |
LMCACHE_BLEND_THRESHOLDS | blend_thresholds | None | 重要token判定閾値(未使用/TODO) |
LMCACHE_BLEND_MIN_TOKENS | blend_min_tokens | 256 | Blend対象の最小トークン数 |
LMCACHE_USE_LAYERWISE | use_layerwise | False | レイヤーワイズ転送(CacheBlendには必須) |
注意: enable_blending=Trueにするとsave_unfull_chunk=Trueが自動設定される(不完全チャンクも保存必要)。
制約・注意事項
対応モデル
- ✅
LlamaForCausalLM(Llama 2/3系) - ✅
Qwen2ForCausalLM(Qwen2系) - ✅
Qwen3ForCausalLM(Qwen3系) - ❌ その他モデル(
NotImplementedError)
非互換機能
- ❌
enable_prefix_caching=True(TODO: 対応予定コメントあり) - ❌ CUDAGraph(
enforce_eager=Trueが必要)
既知のTODO
recomp_ratios[0]しか使わない(複数比率対応TODO)- 異なるレイヤーで異なる比率をサポートするTODO
- 閾値ベースのblendingは未実装
- TP(テンソル並列)、PP、マルチモーダル未サポートのTODO
依存関係
LMCacheConnectorV1Impl.start_load_kv()
└── LMCBlender.blend()
└── blend_layer() [Generator]
├── LMCacheEngine.retrieve_layer() [Generator] ← ストレージからGPUへKV転送
└── LMCBaseModel.compute_layer() [Generator] ← vLLMモデルの独自forward
└── LMCBlender.process_qkv() ← 重要token選択・KV更新
VLLMModelTracker.register_model() ← vLLMパッチ(load_model()末尾)
└── LMCBlenderBuilder.get_or_create() ← blender初期化時に参照
LMCache 用語集
確信度: [VERIFIED] 最終更新: 2026-02-16(Phase 1 セッション1)
コアコンセプト
| 用語 | 説明 |
|---|---|
| LMCacheEngine | KVキャッシュのstore/retrieve/prefetchを統合するメインエンジン。lmcache/v1/cache_engine.py |
| LMCacheManager | LMCacheの内部コンポーネント(Engine, LookupClient, OffloadServer等)のライフサイクル管理。lmcache/v1/manager.py |
| CacheEngineKey | KVキャッシュチャンクの一意識別子。(model_name, world_size, worker_id, chunk_hash, dtype, request_configs)の6タプル。lmcache/utils.py:333 |
| LayerCacheEngineKey | CacheEngineKey + layer_id。レイヤー単位保存時のキー。split_layers()で生成。lmcache/utils.py:392 |
| MemoryObj | KVキャッシュデータを保持する抽象メモリオブジェクト。フォーマット情報とテンソルデータを包含。lmcache/v1/memory_management.py |
| MemoryFormat | KVキャッシュのメモリレイアウト種別。KV_2LTD, KV_T2D, KV_2TD, BINARY, KV_MLA_FMT等。 |
| LMCacheMetadata | モデル名、world_size、worker_id、kv_dtype、kv_shape等のメタ情報。サービングエンジンから抽出。 |
| LMCacheEngineConfig | YAML/環境変数ベースの設定。chunk_size, ストレージ設定, blend設定, P2P設定等。 |
トークン処理
| 用語 | 説明 |
|---|---|
| TokenDatabase | トークン列をチャンクキー列に変換する抽象基底クラス。 |
| ChunkedTokenDatabase | 固定サイズ(default 256トークン)チャンクでプレフィックスハッシュを計算。標準の実装。 |
| SegmentTokenDatabase | セパレータベースでセグメント分割。CacheBlend時に使用。 |
| chunk_size | チャンクのトークン数。デフォルト256。 |
| chunk_hash | チャンクのプレフィックスハッシュ値。vLLMのsha256_cborハッシュ関数を直接利用(完全互換)。 |
| NONE_HASH | プレフィックスハッシュチェーンの初期値。vLLMのkv_cache_utils.init_none_hash()で初期化。 |
| store_mask | store時のマスク。False=already-cached prefix、True=新規トークン。False数はchunk_sizeの倍数必須。 |
ストレージ
| 用語 | 説明 |
|---|---|
| StorageManager | 複数のストレージバックエンドを階層管理。put/get要求を各バックエンドに振り分け。 |
| StorageBackendInterface | ストレージバックエンドの抽象インターフェース。contains/put/get等。 |
| LocalCPUBackend | CPU メモリ上のKVキャッシュストレージ(L1)。hot_cache(OrderedDict)で管理。同期書き込み。 |
| hot_cache | LocalCPUBackendのOrderedDict[CacheEngineKey, MemoryObj]。CachePolicyでEviction管理。 |
| allocator_backend | MemoryObj確保を担当するバックエンド。通常はLocalCPUBackend。 |
| LocalDiskBackend | ディスク上のKVキャッシュストレージ(L2)。 |
| RemoteBackend | リモートストレージ(L3)。connector経由でRedis/S3/Valkey等に接続。 |
| P2PBackend | インスタンス間のP2P KVキャッシュ転送。 |
| NIXLBackend | NVIDIA NIXL経由の高速転送。 |
| GdsBackend | GPUDirect Storage経由の転送。 |
| CachePolicy | Eviction方針。FIFO/LRU/LFU/MRUから選択可能。 |
| Serde | シリアライゼーション/デシリアライゼーション。naive(無圧縮)、CacheGen(圧縮)、KIVI等。 |
GPU連携
| 用語 | 説明 |
|---|---|
| GPUConnectorInterface | GPU KVキャッシュとCPU MemoryObj間のデータ転送抽象インターフェース。to_gpu/from_gpu。 |
| VLLMPagedMemGPUConnectorV2 | vLLMのPaged KVキャッシュ向けGPUコネクタ(非レイヤーワイズ)。全レイヤー一括転送。 |
| VLLMPagedMemLayerwiseGPUConnector | レイヤー単位でKVキャッシュを転送するコネクタ。ジェネレータパターン使用。主要パス。 |
| lmc_ops.single_layer_kv_transfer | CUDAカーネル。vLLMのページドKVキャッシュからslot_mapping経由でデータを抽出/書き戻し。 |
| slot_mapping | トークン位置→vLLMページドメモリのflat slot位置へのマッピング。GPU Tensor。 |
| store_stream | GPU→CPU転送専用CUDAストリーム。メイン計算ストリームとオーバーラップ可能。 |
| load_stream | CPU→GPU転送専用CUDAストリーム。retrieve時にメイン計算とオーバーラップ。 |
| lmc_ops.multi_layer_kv_transfer | CUDAカーネル。全レイヤー一括でMemoryObj→paged KVキャッシュに転送(Bulk retrieve用)。 |
| fused_rotary_emb | RoPE位置補正関数。Layerwise retrieve時に保存時と現在のposition差分を補正。 |
| VLLMBufferLayerwiseGPUConnector | CacheBlend対応のLayerwiseコネクタ。ダブルバッファ+RoPE補正+gap zeroing。 |
統合
| 用語 | 説明 |
|---|---|
| LMCacheConnectorV1Dynamic | vLLMのKVConnectorBase_V1実装。LMCacheConnectorV1Implに委譲。 |
| LMCacheConnectorV1Impl | vLLM統合の実装本体(vllm_v1_adapter.py)。LoadSpec/SaveSpecでロード・保存を管理。 |
| LoadSpec | ロード仕様。vLLMキャッシュ済みトークン数、LMCacheキャッシュ済みトークン数、ロード可否。 |
| SaveSpec | 保存仕様。skip_leading_tokens(キャッシュ済みプレフィックス長)、can_save(保存可否)。 |
| ConnectorMetadata | Scheduler→Worker間で渡されるメタデータ。各リクエストのtoken_ids, slot_mapping, LoadSpec, SaveSpecを含む。 |
| kv_role | "kv_both"(default)/"kv_producer"/"kv_consumer"。producer時はskip_leading_tokens=0。 |
| LookupClient | Scheduler側でキャッシュ存在確認を行うZMQベースクライアント。lmcache_lookup_client.py |
| LookupServer | Worker側でLookupClientからのZMQ REQ/REPを受け付け、StorageManager.async_lookup_and_prefetchを実行。 |
| EventManager | 非同期イベント(LOADING等)のFutureを管理。lookup_idでprefetch結果とretrieve消費を紐付け。 |
| token_mask | retrieve時のマスク。False=vLLMがキャッシュ済み(chunk_sizeの倍数に切り下げ)、True=LMCacheからロード対象。 |
| ret_mask | retrieve結果のマスク。True=LMCacheから実際に取得成功、False=未取得。Engine内部で構築。 |
| write-back | StorageManager.batched_get()がリモートバックエンドから取得した場合、自動的にLocalCPUBackendにコピーする動作。 |
| get_block_mapping | チャンクの所在バックエンドをprefix match方式で特定するStorageManagerメソッド。 |
CacheBlend
| 用語 | 説明 |
|---|---|
| CacheBlend | 非プレフィックス部分のKVキャッシュも再利用する技術。重要トークンを再計算して品質保持。 |
| Blender | CacheBlendのblending計算を実行するコンポーネント。lmcache/v1/compute/blend/ |
| blend_recompute_ratios | 再計算するトークンの割合。 |
| blend_special_str | セグメント分割用セパレータ文字列。デフォルト" # # "。 |
分散・マルチプロセス
| 用語 | 説明 |
|---|---|
| CacheController | 複数LMCacheインスタンス間のキャッシュ状態を中央管理するコントローラ。 |
| LMCacheWorker | CacheControllerと通信するワーカー。Heartbeat/Register/P2P Lookup。 |
| MultiProcess Server | ZMQ IPCベースの別プロセスLMCacheサーバー。SessionManager, GPUCacheContext管理。 |
| BlendServer | CacheBlend用MPサーバー。MPCacheEngine継承。 |
| OffloadServer | KVキャッシュオフロード用ZMQサーバー。 |
| Disaggregated Prefill (PD) | Prefill/Decode分離アーキテクチャ。NIXL経由でPD間転送。 |
設定キー(主要)
| 設定名 | デフォルト | 説明 |
|---|---|---|
chunk_size | 256 | チャンクのトークン数 |
local_cpu | true | CPU バックエンド有効化 |
max_local_cpu_size | 5.0 (GB) | CPUストレージ上限 |
local_disk | None | ディスクパス(Noneで無効) |
remote_url | None | リモートストレージURL |
remote_serde | “naive” | リモート用Serde |
use_layerwise | false | レイヤー単位転送 |
enable_blending | false | CacheBlend有効化 |
enable_p2p | false | P2P転送有効化 |
enable_pd | false | Disaggregated Prefill |
enable_controller | false | CacheController有効化 |
save_decode_cache | false | Decodeフェーズのキャッシュも保存 |