Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

コードリーディング: vLLM & LMCache

LLM推論サービングに関連するOSSのコードリーディング結果を構造化して蓄積するプロジェクト。

対象OSS

OSSソースコード概要Phase
vLLMtarget/vllm/LLM推論サービングエンジンPhase 2
LMCachetarget/LMCache/KVキャッシュ保存・共有・再利用ライブラリPhase 0a

プロジェクト横断

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_idstr内部リクエストID(外部IDに8文字ランダムサフィックス付与)
prompt_token_idslist[int] | Noneトークナイズ済みプロンプト
mm_featureslist[MultiModalFeatureSpec] | Noneマルチモーダル入力(テキスト推論ではNone)
sampling_paramsSamplingParams | Noneサンプリングパラメータ(clone済み)
eos_token_idint | None終了トークンID
arrival_timefloatリクエスト到着時刻
lora_requestLoRARequest | NoneLoRAアダプタ情報
priorityint優先度(デフォルト0)
data_parallel_rankint | 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_reqslist[NewRequestData]初回スケジュールされたリクエスト(フルデータ)
scheduled_cached_reqsCachedRequestData既スケジュール済みリクエスト(差分のみ)
num_scheduled_tokensdict[str, int]リクエストごとのスケジュールトークン数
total_num_scheduled_tokensint合計スケジュールトークン数
scheduled_spec_decode_tokensdict[str, list[int]]Speculative Decoding用トークン
scheduled_encoder_inputsdict[str, list[int]]エンコーダ入力インデックス(マルチモーダル)
num_common_prefix_blockslist[int]共通プレフィックスブロック数(Cascade Attention用)
finished_req_idsset[str]このステップで完了したリクエストID
free_encoder_mm_hasheslist[str]解放するエンコーダキャッシュのmm_hash
preempted_req_idsset[str] | Noneプリエンプションされたリクエスト
has_structured_output_requestsbool構造化出力リクエストの有無
pending_structured_output_tokensboolGrammar bitmask準備状態
num_invalid_spec_tokensdict[str, int] | None無効スペキュレーショントークン数
kv_connector_metadataKVConnectorMetadata | NoneKV Transfer メタデータ
ec_connector_metadataECConnectorMetadata | NoneEC Transfer メタデータ

NewRequestData は初回スケジュール時のフルデータ(プロンプトトークン、サンプリングパラメータ、ブロックID等)を含む。CachedRequestData は既スケジュール済みリクエストの差分(新規ブロックID、新トークンID、計算済みトークン数の更新)のみを含み、プロセス間通信コストを最小化する。

ModelRunnerOutput

GPUModelRunner → EngineCore の境界。モデル推論結果を含む。

参照: target/vllm/vllm/v1/outputs.py:160 (ModelRunnerOutput)

フィールド説明
req_idslist[str]バッチ内のリクエストID一覧
req_id_to_indexdict[str, int]リクエストID → バッチインデックス
sampled_token_idslist[list[int]]サンプリング済みトークンID [num_reqs, num_generated]
logprobsLogprobsLists | None生成トークンの対数確率
prompt_logprobs_dictdict[str, LogprobsTensors | None]プロンプトトークンの対数確率
pooler_outputlist[Tensor | None] | Noneプーリング出力(埋め込みモデル用)
kv_connector_outputKVConnectorOutput | NoneKV Transfer出力
ec_connector_outputECConnectorOutput | NoneEC Transfer出力
num_nans_in_logitsdict[str, int] | Nonelogits内のNaN数
cudagraph_statsCUDAGraphStat | NoneCUDAGraph実行統計

Worker→Executorへの転送ではPythonリスト形式を使用し、torch.Tensorの高コストなシリアライゼーションを回避する。

EngineCoreOutput

バックエンド → フロントエンドの境界。リクエスト単位の推論結果。

参照: target/vllm/vllm/v1/engine/__init__.py:130 (EngineCoreOutput)

フィールド説明
request_idstr対応するリクエストID
new_token_idslist[int]新たに生成されたトークンID
finish_reasonFinishReason | None完了理由(stop/length/abort/error)
new_logprobsLogprobsLists | None生成トークンのlogprobs
num_cached_tokensintプレフィックスキャッシュヒット数

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_idstr外部リクエストID(クライアントが指定したID)
promptstr | None元のプロンプト文字列
prompt_token_idslist[int] | Noneトークナイズ済みプロンプト
prompt_logprobsPromptLogprobs | Noneプロンプトトークンの対数確率
outputslist[CompletionOutput]サンプルごとの出力(n>1で複数)
finishedboolリクエスト完了フラグ
metricsRequestStateStats | Noneレイテンシ等の統計情報
num_cached_tokensint | Noneプレフィックスキャッシュヒット数
kv_transfer_paramsdict[str, Any] | NoneKV Transfer情報(完了時)

CompletionOutput (target/vllm/vllm/outputs.py:23) は各サンプルの出力を表す:

フィールド説明
indexintサンプルインデックス
textstrデトークナイズ済みテキスト
token_idsGenericSequence[int]生成トークンID列
cumulative_logprobfloat | None累積対数確率
logprobsSampleLogprobs | None各トークンのlogprobs
finish_reasonstr | None完了理由(“stop” / “length”)
stop_reasonint | str | None停止トークン/文字列

出力モードRequestOutputKindtarget/vllm/vllm/sampling_params.py:108):

  • CUMULATIVE: 毎回全出力を返す(デフォルト)
  • DELTA: 差分のみ返す(ストリーミング向け)
  • FINAL_ONLY: 完了時のみ返す

上流パス: リクエスト受信 → EngineCore

エントリポイント (LLM / AsyncLLM)

vLLMには同期パス(LLM)と非同期パス(AsyncLLM)の2つのエントリポイントがある。内部的にはどちらもInputProcessorEngineCoreClientを使用する。

非同期パス(主パス): 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 / MsgpackDecodermsgspecライブラリ)
    • 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 1L350-517RUNNINGリクエストトークン予算割当。ブロック不足時はプリエンプション
Phase 2L532-800WAITINGリクエスト新規受け入れ。プレフィックスキャッシュ検索 + ブロック割当
Phase 3L827-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)

トークンからテキストへの変換はインクリメンタルに行われ、ストリーミング出力を実現する。

クラス条件方式
FastIncrementalDetokenizerPreTrainedTokenizerFast 使用時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での深堀り順序。ユーザー関心領域とフロー上の重要度に基づく。

優先度コンポーネント理由現在の深度
SKVCacheManagerユーザー関心1位(メモリ管理/KVキャッシュ)。PagedAttention、ブロック管理、Eviction[MEDIUM]
ASchedulerKVCacheManagerと密連携、推論パイプライン全体を制御。Continuous Batching[MEDIUM]
AGPUModelRunner推論実行の中核。6277行の巨大クラス。将来のプラグイン開発に重要[SHALLOW]
BEngineCorestep()サイクル、batch_queueパイプライン。全体の統合ポイント[MEDIUM]
BOutputProcessorデトークナイズ、停止判定。ストリーミング出力の仕組み[SHALLOW]
CAsyncLLM, InputProcessorエントリポイント。薄いレイヤー[SHALLOW]
CExecutor, Worker委譲パターン。分散推論時のみ詳細が必要[SHALLOW]
CEngineCoreClientZMQ IPC通信層。プロトコルは把握済み[SHALLOW]

参照ファイル一覧

ファイル主要クラス/関数役割
target/vllm/vllm/entrypoints/llm.pyLLM.generate() (L396), _add_request() (L1850)同期エントリポイント
target/vllm/vllm/v1/engine/async_llm.pyAsyncLLM.generate() (L537), add_request() (L286)非同期エントリポイント
target/vllm/vllm/v1/engine/input_processor.pyInputProcessor.process_inputs() (L521)入力処理
target/vllm/vllm/v1/engine/__init__.pyEngineCoreRequest (L55), EngineCoreOutput (L130), EngineCoreOutputs (L176)境界データ構造
target/vllm/vllm/v1/engine/core_client.pyEngineCoreClient (L63), MPClient (L442), AsyncMPClient (L822)ZMQ IPC通信
target/vllm/vllm/v1/engine/core.pyEngineCore.add_request() (L288), step() (L389)推論ループ本体
target/vllm/vllm/v1/core/sched/scheduler.pyScheduler.schedule() (L321), update_from_output() (L1241)スケジューリング
target/vllm/vllm/v1/core/sched/output.pySchedulerOutput (L184), NewRequestData (L34), CachedRequestData (L114)スケジュール出力データ構造
target/vllm/vllm/v1/core/kv_cache_manager.pyKVCacheManager.allocate_slots() (L206), get_computed_blocks() (L164)KVキャッシュ管理
target/vllm/vllm/v1/core/block_pool.pyBlockPool (L128)物理ブロック管理
target/vllm/vllm/v1/request.pyRequestリクエスト内部状態
target/vllm/vllm/v1/outputs.pyModelRunnerOutput (L160)モデル推論出力
target/vllm/vllm/v1/executor/abstract.pyExecutor (ABC), execute_model() (L202), collective_rpc() (L180)実行層抽象
target/vllm/vllm/v1/executor/uniproc_executor.pyUniProcExecutor (L26)単一プロセス実行
target/vllm/vllm/v1/executor/multiproc_executor.pyMultiprocExecutor (L93)マルチプロセス実行
target/vllm/vllm/v1/worker/gpu_worker.pyWorker.execute_model() (L604), sample_tokens() (L598)GPU Worker
target/vllm/vllm/v1/worker/gpu_model_runner.pyGPUModelRunner.execute_model() (L3312), sample_tokens() (L3621), ExecuteModelState (L313)モデル実行
target/vllm/vllm/v1/engine/output_processor.pyOutputProcessor.process_outputs() (L582), RequestState.make_request_output() (L269)出力処理
target/vllm/vllm/v1/engine/detokenizer.pyIncrementalDetokenizer (L30), FastIncrementalDetokenizer (L169), check_stop_strings() (L316)デトークナイズ
target/vllm/vllm/v1/engine/logprobs.pyLogprobsProcessor (L28)logprobs処理
target/vllm/vllm/outputs.pyRequestOutput (L86), CompletionOutput (L23)最終出力データ構造

マルチモーダル推論パスの差分

テキスト推論フローに対し、画像等のマルチモーダル入力がある場合の主要な差分を以下に示す。詳細は マルチモーダル処理パイプライン を参照。

フロントエンド(P0)の差分

  1. チャットテンプレート: プレースホルダー(<start_of_image> 等)がプロンプトに挿入される
  2. HF Processor実行: 画像を pixel_values テンソルに変換(リサイズ、正規化、パッチ分割)
  3. MMハッシュ計算: MultiModalHasher でコンテンツベースのblake3ハッシュを生成
  4. ProcessorCache: HF処理結果をキャッシュ(4種類の実装: processor_only/lru/shm/none)
  5. EngineCoreRequest: mm_features: list[MultiModalFeatureSpec] にテンソルデータ・位置情報・ハッシュを格納

バックエンド(P1)の差分

  1. EncoderCacheManager: エンコーダ出力をリファレンスカウント方式で管理。キャッシュヒットでエンコーダ計算スキップ
  2. Scheduler: encoder_compute_budget でステップあたりのエンコーダ計算量を制御
  3. GPUModelRunner:
    • _execute_mm_encoder(): ビジョンエンコーダ実行(model.embed_multimodal()
    • _gather_mm_embeddings(): キャッシュからプレースホルダー位置に対応する埋め込みを取得
    • embed_input_ids(): masked_scatter_ でテキスト埋め込みとビジョン埋め込みをマージ
  4. モデル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:4LLMEngine = V1LLMEngine の1行エイリアス

v1が現行アーキテクチャの本体であり、コードリーディングでは vllm/v1/ を中心に読む。ただし vllm/model_executor/vllm/distributed/vllm/multimodal/ 等はv1からも直接利用されるため調査対象に含む。

主要コンポーネント

コンポーネントクラスパス役割
AsyncLLMAsyncLLM(EngineClient)target/vllm/vllm/v1/engine/async_llm.py:71非同期APIトップレベル
EngineCoreEngineCoretarget/vllm/vllm/v1/engine/core.py:79推論ループ内側。ZMQで外側と通信
SchedulerScheduler(SchedulerInterface)target/vllm/vllm/v1/core/sched/scheduler.py:63Continuous Batchingスケジューラ
KVCacheManagerKVCacheManagertarget/vllm/vllm/v1/core/kv_cache_manager.py:94KVキャッシュブロック管理
ExecutorExecutor(ABC)target/vllm/vllm/v1/executor/abstract.pyWorker群を束ねる実行層
WorkerWorker(WorkerBase)target/vllm/vllm/v1/worker/gpu_worker.py:701 GPUデバイスを担当
GPUModelRunnerGPUModelRunnertarget/vllm/vllm/v1/worker/gpu_model_runner.py:329GPU上のフォワードパス実行
VllmConfigVllmConfigtarget/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.pyLLMEngine(v1への薄いラッパー)
target/vllm/vllm/v1/engine/async_llm.pyAsyncLLM
target/vllm/vllm/v1/engine/core.pyEngineCore, EngineCoreProc
target/vllm/vllm/v1/core/sched/scheduler.pyScheduler
target/vllm/vllm/v1/core/kv_cache_manager.pyKVCacheManager
target/vllm/vllm/v1/executor/abstract.pyExecutor(ABC)
target/vllm/vllm/v1/worker/gpu_worker.pyWorker
target/vllm/vllm/v1/worker/gpu_model_runner.pyGPUModelRunner
target/vllm/vllm/config/vllm.pyVllmConfig
target/vllm/vllm/entrypoints/llm.pyLLM

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
ロール生成場所主な責務
SCHEDULERScheduler.__init__() via ECConnectorFactoryキャッシュ存在チェック、メタデータ構築
WORKERgpu_worker.pyensure_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

プロパティ

プロパティ説明
roleECConnectorRoleSCHEDULER or WORKER
is_producerboolエンコーダキャッシュを生成する側か
is_consumerboolエンコーダキャッシュを消費する側か

抽象メソッド(実装必須)

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) → ECConnectorMetadataWorker転送用メタデータ構築

具象メソッド(オーバーライド任意)

メソッドデフォルト動作説明
bind_connector_metadataメタデータ保持Worker側: 毎step実行前に呼ばれる
clear_connector_metadataNoneに設定Worker側: 毎step実行後に呼ばれる
register_cachesno-op将来のP2P機能用
get_finished(None, None)非同期転送完了通知
update_connector_outputno-opWorker出力から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_produceris_consumer
ec_producerエンコーダ計算+キャッシュ保存TrueFalse
ec_consumerキャッシュ読み込み+デコーダ実行FalseTrue
ec_both両方の機能TrueTrue

主要設定パラメータ

パラメータデフォルト説明
ec_connectorNoneコネクタ名(例: “ECExampleConnector”)
ec_roleNoneECロール
ec_connector_module_pathNoneカスタムコネクタのPythonモジュールパス
ec_connector_extra_config{}コネクタ固有の追加設定
ec_buffer_device“cuda”バッファデバイス
ec_buffer_size1e9バッファサイズ(バイト)
ec_ip / ec_port127.0.0.1:14579P2P接続用
ec_rank / ec_parallel_sizeNone / 1分散接続設定

ECConnectorFactory

参照: target/vllm/vllm/distributed/ec_transfer/ec_connector/factory.py:20-85

コネクタ登録方式

2つの登録方法がある:

  1. 静的登録: ECConnectorFactory.register_connector() でモジュール遅延ロード登録
  2. 動的ロード: 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)

現在登録済みコネクタ

名前実装用途
ECExampleConnectorexample_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

  1. GPUModelRunnerが _execute_mm_encoder() 完了後に maybe_save_ec_to_connector() を呼ぶ
  2. save_caches(): テンソルを .detach().cpu() してsafetensorsで保存

読み込み(Consumer側)

参照: target/vllm/vllm/distributed/ec_transfer/ec_connector/example_connector.py:63-96

  1. Scheduler側: has_cache_item() でファイル存在確認 (os.path.exists)
  2. Scheduler側: build_connector_meta() でロード対象リストを構築
  3. 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.pyensure_ec_transfer_initialized() を呼ぶことで行われる。Scheduler側は ECConnectorFactory.create_connector() で直接生成し、self.ec_connector に保持する(シングルトンではない)。

カスタムECConnector実装ガイド

最小実装

  1. ECConnectorBase を継承
  2. 5つの抽象メソッドを実装
  3. ECTransferConfigのec_connectorにクラス名、ec_connector_module_pathにモジュールパスを指定

実装の要点

  • has_cache_item() はSchedulerのホットパスで呼ばれるため高速であるべき
  • start_load_caches()encoder_cache dict に直接テンソルを追加する
  • 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(例)、共有メモリ、ネットワーク等(実装依存)

開発状況・未実装機能

  1. ECConnectorOutput未消費: Worker→Scheduler方向の非同期転送完了フィードバックが未実装
  2. request_finished未統合: Schedulerからec_connector.request_finished()が呼ばれていない
  3. register_caches未実装: P2P直接転送のためのキャッシュ登録(TODO)
  4. エンコーダキャッシュ事前割り当て未対応: encoder_cachedict のため、固定バッファへの移行が必要
  5. 登録済みコネクタが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
場所データ構造役割
論理管理SchedulerEncoderCacheManagerキャッシュ容量管理、参照カウント、Eviction判定
物理ストレージGPUModelRunnerdict[str, torch.Tensor]mm_hash → エンコーダ出力テンソルの保持

EncoderCacheManager 詳細

主要フィールド

参照: target/vllm/vllm/v1/core/encoder_cache_manager.py:67-77

フィールド説明
cache_sizeintエンコーダ埋め込み数で測った総容量
num_free_slotsint現在利用可能な空きスロット数
num_freeable_slotsint参照ゼロエントリの回収で即座に利用可能になるスロット数
cacheddict[str, set[str]]mm_hash → 参照中リクエストIDの集合
freeableOrderedDict[str, int]参照ゼロエントリの挿入順リスト(mm_hash → 埋め込み数)
freedlist[str]直近のEvictionで物理解放すべきmm_hashリスト

Eviction方式: FIFO(参照ゼロエントリの遅延解放)

EncoderCacheManagerは遅延解放FIFO方式を採用する:

  1. リクエスト完了時、参照カウントが0になったエントリは即座には解放されず freeable OrderedDictに追加
  2. 新しいエンコーダ出力のキャッシュ確保(can_allocate())時に空きが不足した場合のみ、古い順にEviction
  3. Evictionされたmm_hashは freed リストに追加され、次の get_freed_mm_hashes() でWorkerに通知
  4. 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)Scheduler1エントリの参照解放
free(request)Scheduler(リクエスト完了/中断時)全エントリの参照解放
get_freed_mm_hashes()Scheduler._build_scheduler_outputEviction済み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)
参照カウントありなし
EvictionFIFO遅延解放即時解放(1step遅延バッファあり)
用途Vision-Language ModelEncoder-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_slotsnum_free_slots 以上の値を常に持つ(freeable + free の合計)

EngineCore サマリー

深度: [MEDIUM] 確信度: [VERIFIED] 最終更新: 2026-02-11

概要

EngineCoreはバックエンドプロセス(EngineCoreProc)内で動作する推論ループの中央制御コンポーネントである。SchedulerModelExecutorKVCacheManagerを統括し、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__()L82Scheduler, ModelExecutor, KVキャッシュの初期化
step()L389メインループ: schedule → execute → update
step_with_batch_queue()L434パイプライン並列化版step(batch_queue使用)
add_request()L288リクエストをバリデーション後Schedulerに登録
post_step()L424step後処理(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_batches1batch_queueサイズ(>1でパイプライン並列化)
async_schedulingFalse非同期スケジューリングモード

呼び出しフロー

[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()L856ZMQ出力受信タスクを起動
get_output_async()L902asyncio.Queueから出力を取得
_send_input()L913EngineCoreRequestをZMQで送信
_send_input_message()L925ZMQ 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_size1データ並列数。>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.Structarray_like形式でコンパクトなバイナリ表現
  • zero-copy: ZMQ copy=False でメモリコピーを最小化。テンソルバッキングバッファの追跡(add_pending_message
  • weakref: 出力タスクがクライアントへの循環参照を持たないようweakrefを使用

関連ドキュメント

エントリポイント (AsyncLLM / LLM) サマリー

深度: [SHALLOW] 確信度: [VERIFIED] 最終更新: 2026-02-09

概要

AsyncLLMLLMは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
ParentRequestn>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()L1850InputProcessor→llm_engine.add_request()
_run_engine()L1900ポーリングループ。完了までstep()を繰り返す

設定

パラメータデフォルト説明
log_requestsTrueリクエストログ出力
log_stats引数指定統計ログ出力
start_engine_loopTrueエンジンループ自動起動

呼び出しフロー

[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つのチャネルを併用:

  1. ShmRingBuffer(共有メモリ): 24MiB以下の通常データ。ロックフリー、~20nsメモリフェンスのみ
  2. 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_mqExecutor → 全WorkerRPCコマンドのブロードキャスト
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.pyExecutor, collective_rpc() (L180), execute_model() (L202)
target/vllm/vllm/v1/executor/uniproc_executor.pyUniProcExecutor (L26)
target/vllm/vllm/v1/executor/multiproc_executor.pyMultiprocExecutor (L93), WorkerProc (L493), worker_busy_loop (L845)
target/vllm/vllm/v1/executor/ray_executor.pyRayDistributedExecutor (L62)
target/vllm/vllm/v1/worker/gpu_worker.pyWorker (L70), execute_model() (L604)
target/vllm/vllm/v1/worker/worker_base.pyWorkerBase (L34), WorkerWrapperBase (L175)
target/vllm/vllm/distributed/device_communicators/shm_broadcast.pyShmRingBuffer (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つの実行モードが存在する:

モード説明使用条件
FULLforward全体をキャプチャAttentionバックエンドが対応、cascade attention無効
PIECEWISEAttention以外をキャプチャ(torch.compile統合)Attention部分はコンパイル済みコードで実行
NONEEagerモードCUDAGraph無効、バッチサイズ超過、calc_kv_scales時

CudagraphDispatcher

参照: target/vllm/vllm/v1/cudagraph_dispatcher.py:14

事前キャプチャ済みCUDAGraphの中からランタイムで適切なグラフを選択する:

  1. cudagraph_keys: dict[CUDAGraphMode, set[BatchDescriptor]] にキャプチャ済みのバッチ記述子を保持
  2. dispatch() は入力 num_tokens を最小のパディングサイズに丸め上げ
  3. 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の実行モード判定:

  1. _is_uniform_decode() — 全リクエストがデコードフェーズ(query_len=1)か判定
  2. cudagraph_dispatcher.dispatch() — モードとパディングサイズを決定
  3. Data Parallel時は coordinate_batch_across_dp() で全ランク間で合意

ExecuteModelState

参照: target/vllm/vllm/v1/worker/gpu_model_runner.py:313 (ExecuteModelState)

2フェーズ間の一時状態を保持するNamedTuple。GPUテンソルを含むため、シリアライズはされない。

フィールド説明
scheduler_outputSchedulerOutputスケジュール結果
logitstorch.Tensorモデル出力logits
spec_decode_metadataSpecDecodeMetadata | NoneSpeculative Decoding情報
hidden_statestorch.Tensor隠れ状態
sample_hidden_statestorch.Tensorサンプリング用隠れ状態
aux_hidden_stateslist[torch.Tensor] | None補助隠れ状態
ec_connector_outputECConnectorOutput | Noneエンコーダ出力
cudagraph_statsCUDAGraphStat | NoneCUDAGraph統計
slot_mappingsdict | list | NoneKVキャッシュスロットマッピング

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-3620execute_model()(メインforward)
3621-3934sample_tokens()、PP broadcast、ドラフト提案
3935-4118Speculative Decoding提案
4119-4609モデルロード: load_model(), reload_weights()
4610-5108ダミー実行、プロファイリング
5109-5332CUDAGraphキャプチャ: capture_model(), _capture_cudagraphs()
5333-5596Attentionバックエンド初期化、メタデータビルダー
5597-6152KVキャッシュ初期化: initialize_kv_cache()
6152-6273get_kv_cache_spec(), タイミング統計

マルチモーダル処理

マルチモーダル推論時、GPUModelRunnerは execute_model() 内で以下の追加処理を行う:

  1. _execute_mm_encoder() (L2293): model.embed_multimodal() でビジョンエンコーダ実行。結果を encoder_cache[mm_hash] に格納
  2. _gather_mm_embeddings() (L2449): encoder_cache からスケジュール範囲に対応する埋め込みをスライス。チャンクPrefill対応
  3. 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 Decodingpropose_draft_token_ids()
async_scheduling_update_states_after_model_execute()

主要ファイル

ファイル主要クラス/関数
target/vllm/vllm/v1/worker/gpu_model_runner.pyGPUModelRunner (L329), execute_model() (L3312), sample_tokens() (L3621), ExecuteModelState (L313)
target/vllm/vllm/v1/worker/gpu_input_batch.pyCachedRequestState (L30), InputBatch (L81)
target/vllm/vllm/v1/worker/block_table.pyBlockTable (L16), MultiGroupBlockTable (L253)
target/vllm/vllm/v1/cudagraph_dispatcher.pyCudagraphDispatcher (L14)
target/vllm/vllm/v1/utils.pyCpuGpuBuffer (L105)
target/vllm/vllm/v1/outputs.pyModelRunnerOutput (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_idstrリクエストID
prompt_token_idslist[int] | NoneプロンプトトークンID列
prompt_embedsTensor | Noneプロンプト埋め込み(embedsモード時)
mm_featureslist[MultiModalFeatureSpec]マルチモーダル特徴量
sampling_paramsSamplingParams | Noneサンプリングパラメータ
generatortorch.Generator | Noneシード付き乱数生成器
block_idstuple[list[int], ...]KVキャッシュグループごとのブロックID列
num_computed_tokensint計算済みトークン数(プレフィックスキャッシュ含む)
output_token_idslist[int]生成済みトークンID列
lora_requestLoRARequest | NoneLoRAアダプタ

ライフサイクル: _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,)numpySpec Decode以外のトークン数
num_prompt_tokens(max_num_reqs,)numpyプロンプトトークン数
temperature_cpu / top_p_cpu / top_k_cpu(max_num_reqs,)CPU (pin)サンプリングパラメータ
block_tableMultiGroupBlockTableCpuGpuBufferKVキャッシュブロックテーブル

リクエスト管理

データ構造説明
_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():

  1. 空きインデックスを探す(末尾追加 or 空スロット再利用)
  2. token_ids_cpu にプロンプト+出力トークンIDをコピー
  3. block_table.add_row() でブロックIDを設定
  4. サンプリングパラメータを各テンソルにコピー

remove_request():

  1. req_id_to_index から削除、_req_ids[index] = None で空スロット化
  2. batch_update_builder.removed_append() で空きインデックスを記録
  3. サンプリング関連のset/dictからも除去
  4. テンソル自体はクリアしない(次の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_idstuple[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.pyCachedRequestState (L30), InputBatch (L81), add_request() (L304), remove_request() (L469), condense() (L626)
target/vllm/vllm/v1/worker/block_table.pyMultiGroupBlockTable (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_idsCachedRequestState 作成add_request() で追加
継続(非プリエンプション)new_block_ids を既存 block_idsextend()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() で除去されるが、CachedRequestStateself.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_sizeblock_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転送を発行。

属性説明
cputorch.Tensor (pinned)ピン留めCPUメモリ
gputorch.TensorGPU VRAM
npnp.ndarraycpu の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_giddict[int, Tensor] — KVキャッシュグループID_build_attention_metadata()CommonAttentionMetadata.slot_mapping
slot_mappings_by_layerdict[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)ペアに対して:

  1. attn_group.get_metadata_builder() でビルダーを取得
  2. builder.build(common_attn_metadata=cm)AttentionMetadata を生成
  3. 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.pyBlockTable (L16), compute_slot_mapping() (L133), map_to_kernel_blocks() (L203), MultiGroupBlockTable (L253)
target/vllm/vllm/v1/utils.pyCpuGpuBuffer (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()L521request_id, prompt, paramsEngineCoreRequest
assign_request_id()(別メソッド)EngineCoreRequestNone (内部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は以下の追加処理を行う:

  1. MM Registry から BaseMultiModalProcessor を取得_get_mm_processor()
  2. マルチモーダルデータのパース: mm_processor.info.parse_mm_data()MultiModalDataItems
  3. HF Processor 実行: mm_processor.apply()MultiModalInputs(トークン列 + テンソル + ハッシュ + PlaceholderRange)
  4. ProcessorCache: mm_processor_cache によるHF処理結果のキャッシュ(4種類: processor_only/lru/shm/none)
  5. MultiModalFeatureSpec 構築: データ + 位置情報 + ハッシュを EngineCoreRequest.mm_features にセット

詳細は マルチモーダル フロントエンド処理 を参照。

関連ドキュメント

KVCacheManager サマリー

深度: [DEEP] 確信度: [VERIFIED] 最終更新: 2026-02-11

概要

KVCacheManager は PagedAttention に基づく KV キャッシュブロックの割り当て・解放・プレフィックスキャッシュ検索を管理するクラスである。4層の階層設計(KVCacheManagerKVCacheCoordinatorSingleTypeKVCacheManagerBlockPool)でマルチグループ 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 Attention1 グループ
12 層 Full + 12 層 Sliding Window2 グループ
デコーダ + クロスアテンション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 最適化: KVCacheManagerempty_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 用の先読みトークン

主要コンポーネント

コンポーネント用途ファイル
KVCacheManagerScheduler 向け公開 APItarget/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
FreeKVCacheBlockQueueLRU 順序の空きブロック管理(双方向リンクリスト)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)L143KV キャッシュ使用率 (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 がアテンションタイプごとのブロック管理を担当:

ManagerKVCacheSpecスキップ計算キャッシュ検索
FullAttentionManagerFullAttention / MLA0左→右
SlidingWindowManagerSlidingWindowmax(0, n-w+1)右→左(連続)
ChunkedLocalAttentionManagerChunkedLocal(n//c)*cnull_pad + 左→右
MambaManagerMamban - 1右→左(単一)
CrossAttentionManagerCrossAttentionN/A非対応
SinkFullAttentionManagerSinkFullAttention0左→右

→ 詳細は アテンションタイプ別 Manager を参照

設定

パラメータデフォルト説明
block_sizeモデル依存1 ブロックあたりのトークン数
enable_caching設定依存プレフィックスキャッシュの有効化
num_gpu_blocksプロファイリングで決定GPU メモリから算出される総ブロック数
hash_block_sizeblock_size と同値ハッシュ計算に使用するブロックサイズ
prefix_caching_hash_algosha256_cborハッシュ関数(sha256/sha256_cbor/xxhash/xxhash_cbor)
enable_kv_cache_eventsFalseKV 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.py490KVCacheManager、KVCacheBlocks
kv_cache_coordinator.py586Coordinator 3 実装
single_type_kv_cache_manager.py1065Manager 7 種
block_pool.py490BlockPool、BlockHashToBlockMap
kv_cache_utils.py1644KVCacheBlock、Queue、ハッシュ計算
kv_cache_metrics.py96メトリクス収集

詳細ドキュメント

関連ドキュメント

アテンションタイプ別 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_blocksdefaultdict[str, list[KVCacheBlock]]リクエスト ID → 割り当て済みブロックリスト
num_cached_blockdict[str, int]リクエスト ID → キャッシュ登録済みブロック数。RUNNING リクエストのみ追跡
block_sizeint1 ブロックあたりのトークン数。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_blocksnull_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_idxdict[str, int]前ステップで割り当てた状態ブロックのインデックス
_allocated_block_reqsset[str]ブロック割り当て済みリクエストの集合
num_speculative_blocksintSpeculative 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_lenblock_size の倍数でなければならない
  • sink ブロックは初期化時に popleft_n() で確保され、以降解放されない
  • FullAttentionManager の find_longest_cache_hit()get_num_common_prefix_blocks() をそのまま使用

各 Manager の比較表 [VERIFIED]

Managerスキップ計算キャッシュ検索CascadeDCP/PCPEAGLE
FullAttention0(全トークン)左→右ref_cnt 基準対応対応
SlidingWindowmax(0, n-w+1)右→左(連続)非対応非対応対応
ChunkedLocal(n//c)*cnull_pad + 左→右非対応非対応非対応
Mamban - 1右→左(単一)非対応非対応-
CrossAttention0非対応非対応--
SinkFullAttention0左→右ref_cnt 基準対応対応

関連ドキュメント

BlockPool 詳細

深度: [DEEP] 確信度: [VERIFIED] 最終更新: 2026-02-11

概要

BlockPool はKVキャッシュの物理ブロックを管理するクラスである。ブロックの割り当て・解放・プレフィックスキャッシュ索引を一元管理し、LRU Eviction によるメモリ再利用を実現する。3つの内部データ構造(FreeKVCacheBlockQueueBlockHashToBlockMapKVCacheBlock)で構成される。

参照: 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_idint0 〜 num_gpu_blocks - 1 の一意識別子
ref_cntint参照カウント。0なら空きキュー内(Eviction候補)
_block_hashBlockHashWithGroupId | Noneプレフィックスキャッシュ用ハッシュキー。fullブロックでキャッシュ登録済みの場合のみ設定
prev_free_blockKVCacheBlock | None空きキューの前ノードポインタ
next_free_blockKVCacheBlock | None空きキューの次ノードポインタ
is_nullboolnull_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 オブジェクトのアロケーションを行わず、KVCacheBlockprev_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()L208O(1)先頭ブロックを取り出し
popleft_n(n)L245O(n)先頭から n 個を一括取り出し
remove(block)L278O(1)中間のブロックを除去(touch 用)
append(block)L298O(1)末尾にブロックを追加
append_n(blocks)L321O(n)末尾に複数ブロックを一括追加
get_all_free_blocks()L346O(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)連携のためのイベントシステム。

イベント型発行タイミング用途
BlockStoredcache_full_blocks()新規ブロックがキャッシュ登録された
BlockRemoved_maybe_evict_cached_block()ブロックがキャッシュから除去された
AllBlocksClearedreset_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_secondsidle_secondsreuse_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_keysLoRA、マルチモーダル、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

型階層

定義用途
BlockHashNewType("BlockHash", bytes)ブロック単体のハッシュ値
BlockHashWithGroupIdNewType("BlockHashWithGroupId", bytes)ハッシュ + KV キャッシュグループ ID(4 バイト BE)
ExternalBlockHashbytes | 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 種類のハッシュ関数が利用可能:

名前シリアライゼーションハッシュ出力サイズ特徴
sha256pickleSHA-25632 bytesPython 依存
sha256_cborCBOR (canonical)SHA-25632 bytesデフォルト。言語非依存・再現可能
xxhashpicklexxh3_12816 bytes高速、Python 依存
xxhash_cborCBOR (canonical)xxh3_12816 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()L387MM 入力の identifierブロックと重なる MM 入力のみ
_gen_lora_extra_hash_keys()L451LoRA アダプタの lora_name全ブロック共通
cache_saltL508-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_featuresmm_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

遅延・インクリメンタル計算

  1. 初期化時: Request.__init__()block_hasher が渡された場合、即座に get_hash_new_full_blocks() を呼び、プロンプトの full ブロック分のハッシュを計算
  2. トークン追加時: 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 トラッキング

SingleTypeKVCacheManagernum_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) → NoneMixin(forward前)KVキャッシュの非同期ロード開始
wait_for_layer_load()(layer_name: str) → NoneAttention層内レイヤー別ロード完了待機
save_kv_layer()(layer_name, kv_layer, attn_metadata, **kwargs) → NoneAttention層内レイヤー別KVの非同期セーブ開始
wait_for_save()() → NoneMixin(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) → NoneScheduler._schedule_waiting()ブロック割り当て後のコネクタ状態更新
build_connector_meta()(SchedulerOutput) → KVConnectorMetadataScheduler.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でディスク保存
LMCacheConnectorV1LMCache統合チャンク単位KV保存・3層ストレージ
LMCacheMPConnectorLMCacheマルチプロセス版別プロセスでLMCache実行
NixlConnectorNIXL (RDMA)高速GPU間転送
P2pNcclConnectorP2P NCCLNCCL経由の直接GPU転送
OffloadingConnectorKVオフロードCPU/ディスクへのオフロード
MultiConnector複合コネクタ複数バックエンドを束ねる
MoRIIOConnectorMORIIOMORIIOフレームワーク
MooncakeConnectorMooncake分散学習フレームワーク
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状態に遷移する。

  1. _update_from_kv_xfer_finished(): Worker側コネクタのfinished_recving/finished_sendingを処理
    • finished_recvingfinished_recving_kv_req_idsに追加(次stepで処理)
    • finished_sending → ブロック即時解放
  2. _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_sendingset[str] | None送信完了リクエストID
finished_recvingset[str] | None受信完了リクエストID
kv_connector_statsKVConnectorStats | None転送統計
kv_cache_eventsKVConnectorKVEvents | Noneブロック保存/削除イベント
invalid_block_idsset[int]ロード失敗ブロックID
expected_finished_countintハンドシェイクベースコネクタ用

参照: target/vllm/vllm/v1/outputs.py:123-148

KV Cache Events

外部システム(ルーティング、モニタリング等)へのKVキャッシュ状態通知システム。

イベント型

イベントフィールド発行タイミング
BlockStoredblock_hashes, parent_block_hash, token_ids, block_size, lora_id, mediumKVブロック保存時
BlockRemovedblock_hashes, mediumKVブロック削除時
AllBlocksClearedなし全ブロッククリア時

参照: target/vllm/vllm/distributed/kv_events.py:49-84

イベントフロー

  1. Worker側コネクタがBlockStored/BlockRemovedイベントを生成
  2. _get_kv_connector_output()がイベントをKVConnectorOutput.kv_cache_eventsに収集
  3. Scheduler側でupdate_connector_output()KVEventAggregatorで全Worker共通イベントを集約
  4. take_events()で集約済みイベントを取得
  5. EventPublisher(ZMQ PUB/ROUTERまたはNull)で外部に配信

EventPublisher

実装用途
NullEventPublisherno-op(デフォルト)
ZmqEventPublisherZMQ PUB/ROUTERで配信。インメモリreplay buffer付き

参照: target/vllm/vllm/distributed/kv_events.py:205-473

KVTransferConfig

KV Transferの設定。--kv-transfer-configで指定する。

フィールドデフォルト用途
kv_connectorNoneコネクタ名(“LMCacheConnectorV1“等)
kv_roleNone"kv_producer" / "kv_consumer" / "kv_both"
engine_idUUIDエンジン識別子
kv_buffer_device“cuda”バッファデバイス
kv_buffer_size1e9バッファサイズ(バイト)
kv_rankNoneP/D内のランク(0=prefill, 1=decode)
kv_parallel_size1並列インスタンス数
kv_ip / kv_port“127.0.0.1” / 14579接続先
kv_connector_extra_config{}コネクタ固有追加設定
kv_connector_module_pathNone動的ロード用モジュールパス
kv_load_failure_policy“recompute”ロード失敗時ポリシー(“recompute” or “fail”)

参照: target/vllm/vllm/config/kv_transfer.py:17-117

ECConnectorとの比較

観点KV TransferECConnector
対象デコーダKVキャッシュエンコーダキャッシュ
基底クラスKVConnectorBase_V1ECConnectorBase
abstractメソッド数75
ロール分離SCHEDULER / WORKERSCHEDULER / WORKER
FactoryKVConnectorFactoryECConnectorFactory
MixinKVConnectorModelRunnerMixinECConnectorModelRunnerMixin
レイヤー別操作あり(save/load per layer)なし(エンコーダ出力一括)
非同期ロードあり(WAITING_FOR_REMOTE_KVS)なし
イベント通知あり(BlockStored/Removed)なし
登録済み実装数102(Example, SHM)
設定クラスKVTransferConfigECTransferConfig

ディレクトリ構造

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外部キャッシュ問い合わせ、メタデータ構築、完了処理
GPUModelRunnerMixinでライフサイクル管理、KVキャッシュ登録
Attention層wait_for_layer_load() / save_kv_layer() 呼び出し

下流(KV Transferが使う側)

コンポーネント使い方
KVCacheManagerブロック割り当て・解放情報の取得
ForwardContextKVキャッシュテンソルへのアクセス
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_REGISTRYvllm/multimodal/registry.pyモデルごとのプロセッサ/情報を登録・取得
BaseMultiModalProcessorvllm/multimodal/processing/processor.pyHFプロセッサ実行、プロンプト更新管理
MultiModalHashervllm/multimodal/hasher.pyコンテンツベースハッシュ(blake3)
ProcessorCache (4種)vllm/multimodal/cache.pyP0側のHF処理結果キャッシュ
EncoderCacheManagervllm/v1/core/encoder_cache_manager.pyP1側のエンコーダ出力の論理管理
encoder_cachevllm/v1/worker/gpu_model_runner.py:439GPU上のエンコーダ出力テンソルキャッシュ

キャッシュの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> 等)
EngineCoreRequestmm_features = Nonemm_features = [MultiModalFeatureSpec, ...]
SchedulerKVキャッシュ予算のみ+ エンコーダ計算予算管理
GPUModelRunnerinput_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.mdGemma3: SiglipVisionModel、MultiModalProjector、Pan-and-Scan、masked_scatter_マージ

上流・下流

主要ファイル

ファイル概要
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.pyGPU上でのエンコーダ実行と埋め込みマージ
target/vllm/vllm/model_executor/models/gemma3_mm.pyGemma3のマルチモーダル実装
target/vllm/vllm/model_executor/models/siglip.pySiglipVisionModel(ビジョンエンコーダ)

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 = 16num_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_ の動作:

  1. is_multimodal: (seq_len,) のboolテンソル(True = 画像プレースホルダー位置)
  2. is_multimodal.unsqueeze(-1): (seq_len, 1) → ブロードキャストで (seq_len, hidden_size) に展開
  3. mm_embeds_flat: True位置の数 × hidden_size の連続テンソル
  4. 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.pyGemma3ForConditionalGenerationL481
target/vllm/vllm/model_executor/models/gemma3_mm.pyGemma3MultiModalProjectorL432
target/vllm/vllm/model_executor/models/gemma3_mm.pyGemma3ProcessingInfo, Gemma3MultiModalProcessorL77, L276
target/vllm/vllm/model_executor/models/siglip.pySiglipVisionModelL848
target/vllm/vllm/model_executor/models/siglip.pySiglipVisionEmbeddingsL282
target/vllm/vllm/model_executor/models/siglip.pySiglipEncoderL520
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_sizeintキャッシュ容量(エンコーダ埋め込み数単位)
num_free_slotsint現在の空き容量
num_freeable_slotsint回収可能な容量(参照なしエントリ含む)
cacheddict[str, set[str]]mm_hash → 参照中のrequest_id集合
freeableOrderedDict[str, int]mm_hash → 埋め込み数(参照なし、回収可能)
freedlist[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_inputsdict[str, list[int]]req_id → エンコーダ入力インデックスのリスト
free_encoder_mm_hasheslist[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_tokensnum_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消費しないステップごとに予算管理
SchedulerOutputscheduled_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_idsinputs_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.pyEncoderCacheManagerL17
target/vllm/vllm/v1/core/sched/scheduler.py_get_encoder_budget()L1060
target/vllm/vllm/v1/core/sched/output.pySchedulerOutput (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つの用途がある:

  1. ProcessorCache — HFプロセッサの処理結果キャッシュ(同じ画像の再処理回避)
  2. プレフィックスキャッシュの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_uuidNone画像データから 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そのまま返す
strUTF-8エンコードmodel_id 等
int / floatnp.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.Tensornumpy変換。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_hashidentifier
定義場所MultiModalFeatureSpec.mm_hashMultiModalFeatureSpec.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_updatesitem=None を返す(P1にデータあり、IPC不要)
ShmObjectStoreSenderCache (L437)P0共有メモリ参照 + prompt_updatesitem=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不要)

キャッシュヒット時にスキップされる処理

  1. HF Processor実行(画像のリサイズ、正規化、パッチ分割 → pixel_values テンソル生成)
  2. テンソルデータのIPC送信SenderCache/ShmCache 使用時、data=None にしてZMQ転送量削減)
  3. プロンプト更新の再計算は常に必要(キャッシュに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_idxsargsort_mm_positions() でプロンプト内の出現順にソートされる。

MultiModalFeatureSpec の構造

参照: target/vllm/vllm/multimodal/inputs.py:337-381

フィールド説明
dataMultiModalKwargsItem | None処理済みテンソルデータ。P0キャッシュヒット時は None
modalitystr"image", "video", "audio"
identifierstrエンコーダキャッシュ用ハッシュ(LoRAプレフィックス付きの場合あり)
mm_positionPlaceholderRangeプロンプト内のプレースホルダー位置
mm_hashstr | Noneプロセッサキャッシュ用ハッシュ(LoRAプレフィックスなし)

PlaceholderRange の構造

参照: target/vllm/vllm/multimodal/inputs.py:170-240

フィールド説明
offsetintプロンプト内の開始位置
lengthintプレースホルダーの長さ(トークン数)
is_embedTensor[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.pyInputProcessor, process_inputs(), _get_mm_identifier()L56, L521, L490
target/vllm/vllm/inputs/preprocess.pyInputPreprocessor, _process_multimodal(), _get_mm_processor()L60, L193, L182
target/vllm/vllm/multimodal/hasher.pyMultiModalHasher, hash_kwargs(), serialize_item()L50, L154, L52
target/vllm/vllm/multimodal/cache.pyMultiModalProcessorOnlyCache, SenderCache, ShmCache, ReceiverCacheL326, L379, L437, L614
target/vllm/vllm/multimodal/registry.pyMULTIMODAL_REGISTRY, processor_cache_from_config()L305
target/vllm/vllm/multimodal/inputs.pyMultiModalFeatureSpec, PlaceholderRangeL337, L170
target/vllm/vllm/v1/engine/__init__.pyEngineCoreRequestL55
target/vllm/vllm/model_executor/models/gemma3_mm.pyGemma3MultiModalProcessor, Gemma3ProcessingInfoL276, 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_idstr外部リクエストID(クライアント向け)
detokenizerIncrementalDetokenizerデトークナイザインスタンス
logprobs_processorLogprobsProcessorlogprobs処理インスタンス
output_kindRequestOutputKind出力モード(CUMULATIVE/DELTA/FINAL_ONLY)
queueRequestOutputCollector | NoneAsyncLLM用出力キュー
prompt_token_idslist[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)

モード動作用途
CUMULATIVE0毎回全出力テキスト/トークンを返すデフォルト
DELTA1差分(新規テキスト/トークン)のみ返すストリーミング
FINAL_ONLY2完了時のみ出力を返すバッチ処理

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.pyOutputProcessor (L73), process_outputs() (L582), RequestState (L116), make_request_output() (L269)
target/vllm/vllm/v1/engine/detokenizer.pyIncrementalDetokenizer (L30), FastIncrementalDetokenizer (L169), SlowIncrementalDetokenizer (L258), check_stop_strings() (L316)
target/vllm/vllm/v1/engine/logprobs.pyLogprobsProcessor (L28)
target/vllm/vllm/outputs.pyRequestOutput (L86), CompletionOutput (L23)
target/vllm/vllm/sampling_params.pyRequestOutputKind (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()L1644WAITINGキューにリクエスト登録
update_from_output()L1241ModelRunnerOutputから出力生成 → EngineCoreOutputs
finish_requests()L1666リクエストを完了/中止状態にする
_preempt_request()L898プリエンプション実行(ブロック解放→WAITINGに戻す)
_make_cached_request_data()L999CachedRequestData(差分データ)構築
_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_tokensnum_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リクエストに対してのみ発動:

  1. ポリシーに基づき最低優先度のリクエストを選択
    • Priority: (priority, arrival_time) が最大のリクエスト
    • FIFO: 最後のリクエスト
  2. kv_cache_manager.free(request) でブロック解放
  3. request.status = RequestStatus.PREEMPTEDnum_computed_tokens = 0 にリセット
  4. 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_threshold0長プロンプト分割閾値(0=無効)
scheduling_policyPriorityプリエンプション選択ポリシー

呼び出しフロー

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領域の変更を提案:

  1. Attention Kernel: per-tokenマスキングパラメータの追加(FlashInferは既にサポート)
  2. KV Connector: 成功ビットマップの返却(現在のsequentialなprefix長ではなく)
  3. 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では以下の技術的障壁がある:

  1. トークン化の不一致: blend_special_str(例: " # #")が前後のコンテキストにより異なるトークンIDに変換される。HTTP API経由ではトークン化を制御不可能
  2. /v1/chat/completionsinput_ids を受け付けない: セグメント境界の正確な指定が不可能
  3. /v1/completionsinput_ids を渡してもキャッシュのロードが発生しない: ストアは行われるがリユースなし
  4. 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):

vLLMLMCacheCacheBlend
0.9.20.3.3 / 0.3.7Production Ready
0.10.00.3.7Not supported
0.11.00.3.7Not supported
0.10.00.3.12Production Ready
0.11.00.3.12Production Ready

※ Ascend NPU版マトリクス。GPU版LMCacheとは異なる可能性あり。

タイムライン

日付イベント
2025-06-07CacheBlend V1 初期実装が LMCache にマージ (PR#762)
2025-07-24オンライン推論での動作不良が初報告 (#1136)
2025-08-05トークン化不一致問題の根本原因が特定
2025-09-30vLLM本体に Generalized KV Cache Reuse RFC提出 (#25950)
2025-10-31vllm serve サポートの明確な Feature Request (#1936)
2025-12-29layerwise/blending修正PR提出 (PR#2329)
2026-01-12vLLM RFC#25950にてサブリクエスト分割アプローチ発見の報告
2026-01-27ガーブル出力バグ報告 (#2496)
2026-02-03layerwise KVキャッシュ破損の新規報告 + 特殊トークンワークアラウンド提案

結論・所見

オンライン推論(vllm serve)の現状

動作しない。オフライン専用の状態が約8ヶ月続いている。根本的な問題はCacheBlendのセグメント区切りがトークンレベルの精密な制御を要求するのに対し、HTTP APIがテキストレベルの入力しか受け付けない点にある。

2つのアプローチが存在するが、いずれも未完成:

  1. LMCache側のアプローチ: vLLMのworkerにパッチを当てて対応。バージョン間の互換性維持が困難
  2. 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() が呼ばれる:

  1. セパレータ分割: 入力トークン列をRAGチャンク区切り(" # # " など)で段落に分割
  2. 外部KVロード: 段落単位でLMCacheストレージからKVキャッシュを取得 → 中間GPUバッファに配置
  3. RoPE位置補正: 保存時の位置エンコーディングを新しい位置に変換(FusedRope カスタムCUDAカーネル)
  4. 独自forward実行: vLLMモデルのレイヤー群を使って入力を再計算
  5. 重要token同定: check_layersレイヤーで新旧Kの差分L2ノルムを計算、topk個を選択
  6. 選択的アテンション再計算: 選択されたtokenのみでattentionを再計算(FlashAttn or FlashInfer sparse)
  7. KVマージ: 古いKV(外部ストレージ由来)に重要tokenのKVのみ上書き
  8. 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処理
LlamaLMCLlamaModelなし(そのまま)
Qwen2LMCLlamaModel(流用)なし(そのまま)
Qwen3LMCQwen3Modelq_norm/k_norm 適用

3モデルのみ。新モデル追加には LMCBaseModel の継承と _process_qkv() 実装が必要。

参照: target/LMCache/lmcache/v1/compute/models/utils.py:14-35

アテンションバックエンド

バックエンドクラス用途
FlashAttentionLMCFlashAttnBackend密なアテンション計算(デフォルト)
FlashInfer SparseHackBSAWrapper + 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

パッチが必要な理由

  1. モデルオブジェクトへの直接アクセス: VLLMModelTracker にvLLMのモデルオブジェクトを登録する必要がある。CacheBlendはこのオブジェクトの .model.layers[i] に直接アクセスして独自forwardを実行する
  2. 初期化順序の変更: KV Transfer初期化をモデルロード後に移動する必要がある(モデルオブジェクトが存在しないとblender構築に失敗)
  3. 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との違い:

  1. 中間GPUバッファ: ロード結果をpaged memoryではなく連続バッファに保持(get_kv(layer_id)でblenderがアクセス)
  2. RoPE位置補正: batched_to_gpu()ジェネレータ内で fused_rotary_emb による位置エンコーディング変換を実行
  3. ギャップゼロイング: RAGチャンク間のセパレータ位置をゼロで埋める(current_gap_positions
  4. ダブルバッファ: 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.pyMPCacheEngine を継承した BlendEngine クラスを提供する。ZMQメッセージキューで別プロセスとして動作し、以下の追加APIを持つ:

API用途
cb_register_kv_cacheblendエンジンの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_blendingFalseCacheBlendモード有効化
blend_check_layersNone重要token判定を行うレイヤーIDリスト
blend_recompute_ratiosNone再計算するtokenの割合リスト(現在は[0]のみ使用)
blend_thresholdsNone閾値ベースblending用(未実装
enable_sparse (extra_config)FalseFlashInfer スパースアテンション使用

参照: target/LMCache/lmcache/v1/config.py:101-118

暗黙の前提

  • use_layerwise=True が必須(blending有効時に自動選択: VLLMBufferLayerwiseGPUConnector
  • save_unfull_chunk=True が自動設定される
  • SegmentTokenDatabase が使用される(ChunkedTokenDatabaseではなく)

制約・未実装機能

  1. vLLM本体パッチ必須 — プラグインのみでは動作しない
  2. 対応モデル3種のみ — Llama, Qwen2, Qwen3。新モデルはLMCBaseModel継承が必要
  3. RoPE制約rotary_dim == head_sizerope_scaling=Nonepartial_rotary_factor=1.0 のみ
  4. プレフィックスキャッシュ非互換 — TODOコメント、blending後のプレフィックスキャッシュスキップ
  5. TP/PP未対応 — TODOコメント
  6. マルチモーダル未対応 — TODOコメント
  7. バッチサイズ1前提 — FlashAttnMetadata初期化で batch_size=1 ハードコード
  8. レイヤー別ratio未実装recomp_ratios[0]のみ使用
  9. CUDAGraph未対応 — 独自forward pathのためtorch.compileのみ

関連ドキュメント

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つの選択肢を提示:

  1. 中間バッファ方式: 転送前にコピーする
  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前処理重複排除#27094RFC前処理の50%コスト
エンコーダキャッシュ事前割り当て#33714 コメント提案段階dict→固定バッファ
NIXL ECConnector#32659 タスクfork作業中

6. 主要コントリビューター

名前GitHub役割
Chenguang Zhengfake0fanECConnector基盤設計者、EPDフォローアップ主導
NickLuccheNickLucchevLLM Collaborator、アーキテクチャ方針決定者
Cyrus LeungDarkLight1337vLLM Member、マルチモーダルレビューアー
Qi Wangfurionw (NVIDIA)ec_both ロール貢献
H. JhooMerHSNIXL方式提案者
PiratePaiPiratePaiSHMConnector 実装者

7. プラグイン開発への示唆 [INFERRED]

独自ECConnectorプラグインを開発する場合の留意点:

  1. 現在の安定インタフェース: ECConnectorBase の6メソッドは安定しているが、今後のリファクタリング(事前割り当て方式への移行)で変更の可能性あり
  2. OOTプラグイン: ec_connector_module_path で外部モジュールロード可能。ECConnectorFactory 経由で登録
  3. 転送方式の選択: Mooncake方式が統一バックエンドとして推奨される方向。独自実装よりMooncakeの上に構築する方が将来的に有利
  4. エンコーダキャッシュ管理の変更予定: 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 方式FIFOOrderedDict.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 では同一ドキュメント画像が異なるクエリから繰り返し参照される。

  1. FIFO Eviction との相性の悪さ: 高頻度アクセス画像でも新しいエントリが来れば古い順に追い出される
  2. GPU メモリの有限性: encoder_cache_size を大きくしてもデコーダ KV Cache と競合
  3. 再起動耐性がない: 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]

ECConnectorBaseKVConnectorBase_V1 とは完全に独立した抽象基底クラス。

メソッド分類説明
start_load_caches(encoder_cache, **kwargs)Worker外部ストレージから encoder_cache dict にテンソルをロード
save_caches(encoder_cache, mm_hash, **kwargs)Workerencoder_cache から外部ストレージにテンソルを保存
has_cache_item(identifier)Scheduler外部ストレージにキャッシュが存在するか確認
update_state_after_alloc(request, index)Schedulerアロケーション後の内部状態更新
build_connector_meta(scheduler_output)SchedulerScheduler → 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_connectorstr | NoneNoneコネクタ名(例: "ECExampleConnector"
ec_roleECRole | NoneNone"ec_producer" or "ec_consumer"
ec_connector_extra_configdict{}コネクタ固有の追加設定
ec_connector_module_pathstr | NoneNone動的ロード用モジュールパス
engine_idstr | Noneuuid4 自動生成エンジン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_cache dict に直接格納 → _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_hashnum_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 TransferECConnector
設計目的デコーダ 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:477
    kv_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」する。

差が出るケース:

  1. 画像 A が freeable に入る(参照解放)
  2. 画像 B が freeable に入る
  3. 画像 A が再度参照される → freeable から取り出されて active に戻る
  4. 画像 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.pycheck_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大(コーパス全体)
レイテンシナノ秒マイクロ〜ミリ秒
EvictionLRU(提案変更後)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

登録方法:

  1. ファクトリ登録: ECConnectorFactory.register_connector("RedisECConnector", "my.module", "RedisECConnector")
  2. または動的ロード: --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 を活用したオフラインプリコンピュートの流れ:

  1. Producer モードで vLLM を起動し、コーパス全画像を含むダミーリクエストを送信
  2. save_caches() でエンコーダ出力がストレージに蓄積される
  3. Consumer モードで本番 vLLM を起動
  4. リクエスト到着時に has_cache_item()start_load_caches() でストレージからロード
  5. エンコーダ計算をスキップし、ストレージからの読み出し + GPU 転送のみで処理

7. 残る設計上の考慮事項

7.1 ECExampleConnector の同期 I/O

現在の ECExampleConnectorstart_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(ストレージ)には残る。次にアクセスされた時:

  1. Scheduler: check_and_update_cache() → L1 MISS
  2. Scheduler: ec_connector.has_cache_item() → L2 HIT
  3. 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. 次のステップ

  1. FIFO→LRU の実装: encoder_cache_manager.py の 2 メソッドを修正(変更量: 数行)
  2. カスタム ECConnector の実装: Redis バックエンドの ECConnector を作成(参照: ECExampleConnector の 199 行)
  3. ベンチマーク: RAG ワークロードでの比較
    • ベースライン: FIFO + インメモリのみ
    • 改善 1: LRU + インメモリのみ
    • 改善 2: LRU + Redis ECConnector
  4. コミュニティ調査: 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. キャッシュ比較テーブル

ProcessorCacheEncoderCacheKVプレフィックスキャッシュ
場所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 dictKVCacheManager (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_tokenfull_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種の実装

実装用途格納先ヒット時の動作
MultiModalProcessorOnlyCacheP0完結(IPC無効時)P0メモリテンソル+prompt返却
MultiModalProcessorSenderCacheP0→P1(LRUモード)P0にメタデータのみdata=Noneで送信、IPC転送省略
ShmObjectStoreSenderCacheP0→P1(共有メモリ)共有メモリ共有メモリ参照を返却
MultiModalReceiverCacheP1側(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したもの
  • 論理管理: 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.identifierextra_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.pyMultiModalHasher, hash_kwargs(), serialize_item()L50, L154, L52
target/vllm/vllm/multimodal/cache.pyMultiModalProcessorOnlyCache, SenderCache, ShmCache, ReceiverCacheL326, 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.pyprocessor_cache_from_config()L305
target/vllm/vllm/v1/engine/input_processor.py_get_mm_identifier()L490
target/vllm/vllm/v1/core/encoder_cache_manager.pyEncoderCacheManager, check_and_update_cache()L17, L91
target/vllm/vllm/v1/worker/gpu_model_runner.pyencoder_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.pyget_computed_blocks()L164
target/vllm/vllm/v1/core/sched/scheduler.py_get_encoder_budget()L1060

関連ドキュメント

Gemma3 27B ビジョンパイプライン: 形状フローと数値まとめ

モデルパラメータ(config.json + preprocessor_config.json)

パラメータ出典
image_size896vision_config
patch_size14vision_config
vision hidden_size1152vision_config
vision num_heads16vision_config
vision num_layers27vision_config
text hidden_size5376text_config
text num_heads32text_config
text num_layers62text_config
mm_tokens_per_image256config.json
image_token_index262144config.json
boi_token_index255999config.json
eoi_token_index256000config.json

導出値

導出パラメータ計算
patches_per_image896 / 1464
エンコーダ入力パッチ数64²4096
tokens_per_side√25616
AvgPool2d kernel_size64 / 164
Projector 出力トークン/画像16²256 (= mm_tokens_per_image ✅)

Pan-and-Scan 設定

パラメータpreprocessor_config.jsonフォールバックデフォルト出典
do_pan_and_scannullFalseprocessing_gemma3.py L44
pan_and_scan_min_crop_sizenull256processing_gemma3.py L45
pan_and_scan_max_num_cropsnull4processing_gemma3.py L46
pan_and_scan_min_ratio_to_activatenull1.2processing_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 数11 + num_crops (= 3)
<image> トークン数256256 × (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_patchestensor([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                 │
                        └──────────────────────────────┘

注意事項

  1. ビジョン埋め込みの正規化: テキスト埋め込みには embed_tokens(ids) × normalizer のスケーリングが適用されるが、ビジョン埋め込みには mm_soft_emb_norm(RMSNorm)のみが適用され、normalizer スケーリングは適用されない。

  2. V1 での制限: Pan-and-Scan 有効時、V1 エンジンでは画像トークン間の双方向アテンションが簡略化されたパターンで実装されており、元モデルのアテンションパターンと完全には一致しない。

  3. AvgPool2d の役割: エンコーダは 4096 パッチ(64×64 グリッド)の高解像度で処理しつつ、AvgPool2d(k=4, s=4) で 256 トークン(16×16)に圧縮して LLM に渡す。これにより計算量と情報量のバランスを取っている。

  4. Pan-and-Scan のプロンプト: クロップありの場合のみ、Processor が “Here is the original image … and here are some crops to help you see better …” という装飾テキストを自動挿入する。クロップなしの場合この装飾テキストは存在せず、<image> トークン列のみとなる。ユーザーはクロップの存在を意識する必要はない。

  5. token_id=262144 の扱い: <image> トークンの token_id=262144 は通常の vocab 範囲外(OOV)。handle_oov_mm_token=True により安全にゼロ埋めされ、後続の masked_scatter_ でビジョン埋め込みに上書きされる。

  6. Pan-and-Scan のデフォルト値の出典: min_ratio_to_activate=1.2 等の値は Google がモデルと共に配布した設定ではなく(preprocessor_config.json では全て null)、HF transformers の processing_gemma3.py 内の Gemma3ProcessorKwargs._defaults にハードコードされたフォールバック値。


主要ファイル参照

ファイル主要クラス/関数
vllm/…/gemma3_mm.pyGemma3ForConditionalGeneration, Gemma3MultiModalProjector, Gemma3ProcessingInfo
vllm/…/siglip.pySiglipVisionModel, SiglipVisionEmbeddings, SiglipEncoder
vllm/…/utils.py_merge_multimodal_embeddings()
HF transformers/…/processing_gemma3.pyGemma3Processor, Gemma3ProcessorKwargs (デフォルト値定義)
HF transformers/…/image_processing_gemma3.pyGemma3ImageProcessor (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
バックエンド設定容量目安
L1LocalCPUBackendlocal_cpu=True, max_local_cpu_size~5GB
L2LocalDiskBackendlocal_disk, max_local_disk_size~数十GB
L3RemoteBackendremote_url無制限

追加バックエンド: P2PBackend(GPU直接), GdsBackend(GPU Direct Storage), NixlStorageBackend(RDMA), PDBackend(P/D分離用)

参照: target/LMCache/lmcache/v1/storage_backend/

リモートコネクタ(15+実装)

コネクタURLスキーム用途
RedisConnectorredis://Redis単体
RedisSentinelConnectorredis-sentinel://Redis Sentinel
RedisClusterConnectorredis:// (cluster)Redisクラスタ
S3Connectors3://AWS S3
FSConnectorfs:///ローカルファイルシステム
MooncakestoreConnectormooncakestore://Mooncake
ValkeyConnectorvalkey://Valkey
EICConnectorinfinistore://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
パスソース用途
nativetarget/vllm/.../lmcache_integration/vllm_v1_adapter.pyvLLM同梱版。安定性重視
latesttarget/LMCache/lmcache/integration/vllm/vllm_v1_adapter.pyLMCache最新版。機能追加優先

参照: 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            # 保存スキップフラグ

ライフサイクル:

  1. from_new_request() — 新規リクエスト時にSchedulerOutputから生成
  2. update() — 各stepで新トークン・新ブロック追加
  3. 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               # セーブ実行するか

セーブ判定ロジック

以下のいずれかに該当する場合、セーブをスキップ:

  1. 既に保存済み(num_saved_tokens > 0)で、未保存トークンがチャンク境界に達していない
  2. デコードフェーズでsave_decode_cache=False
  3. リクエスト設定でlmcache.skip_save=True
  4. 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レイヤー別レイヤーごとに個別処理
VLLMBufferLayerwiseGPUConnectorBlending用中間バッファ経由でブレンディング

参照: 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分離時のフロー:

  1. Producerインスタンス(kv_role=kv_producer)がプリフィル実行
  2. KVキャッシュをLMCache経由で保存/転送
  3. Consumerインスタンス(kv_role=kv_consumer)がKVキャッシュをロード
  4. 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_size256チャンクサイズ(トークン数)
local_cpuTrueCPUキャッシュ有効化
max_local_cpu_size5.0CPU最大容量(GB)
local_diskNoneディスクキャッシュパス
remote_urlNoneリモートストレージURL
enable_async_loadingTrue非同期ロード
use_layerwiseFalseレイヤー別処理
enable_blendingFalseCacheBlendモード
save_decode_cacheFalseデコードKVも保存
save_unfull_chunkTrue部分チャンク保存
enable_pdFalseP/D分離モード

vLLM extra_configからの設定伝搬

kv_connector_extra_configlmcache.プレフィックス付きの設定を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側の EncoderCachedict[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

キャッシュヒット時にスキップされる処理:

  1. HF Processorの実行(画像リサイズ・正規化・パッチ分割)
  2. テンソルデータのZMQ IPC転送(lru/shmモード時)

2. Config設定項目

参照: target/vllm/vllm/config/multimodal.py:103MultiModalConfig

フィールド名CLIオプションデフォルト制約説明
mm_processor_cache_gb--mm-processor-cache-gb4≥0キャッシュ容量(GiB)。0 で完全無効
mm_processor_cache_type--mm-processor-cache-type“lru”"lru" or "shm"キャッシュ実装タイプ
mm_shm_cache_max_object_size_mb(直接CLIオプションなし)128≥0shmモード時の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_lockmultiprocessing.Lock)で参照カウントを保護。


5. LRUキャッシュの内部実装

参照: target/vllm/vllm/utils/cache.py:51vllm.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:175BaseMultiModalCache 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_gb GiBを確保する
    • IPC非対応になり processor_only モードに自動フォールバック

mm_cacheによる実質的な高速化

同一画像が何度も送られる場合(例: 同じシステム画像を全リクエストで使用)、キャッシュヒット時は HF Processor の実行を完全にスキップできるため、事実上の並列化効果が得られる。


9. キャッシュモード別データフロー比較

processor_onlylrushm
P0保持テンソル + prompt_updatesサイズメタデータ + prompt_updatesSHMアドレス + prompt_updates(P0-private dict)
P1保持なしテンソル(LRU)共有メモリ上のテンソル
エビクションLRULRU(P0/P1連動)FIFO
IPCヒット時テンソルをZMQ送信data=None 送信(テンソル省略)data=None 送信(SHMアドレスのみ)
MissのZMQ転送テンソル全体テンソル全体data=None(SHM経由)
適用場面マルチAPIプロセス / DP≥2単一APIプロセス + 単一DP(デフォルト)同左(マルチWorkerで転送削減)

関連ドキュメント

主要ファイル

ファイル主要クラス/関数
target/vllm/vllm/config/multimodal.pyMultiModalConfig(mm_processor_cache_gb等)L103
target/vllm/vllm/multimodal/cache.pyMultiModalProcessorOnlyCache, SenderCache, ShmObjectStoreSenderCache, ReceiverCache, ShmObjectStoreReceiverCacheL326, 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.pyLRUCache(cachetools継承、サイズベース、ピン機能)L51
target/vllm/vllm/distributed/device_communicators/shm_object_storage.pySingleWriterShmRingBuffer, SingleWriterShmObjectStorageL22, L412
target/vllm/vllm/v1/engine/input_processor.pyprocess_inputs()set_default_torch_num_threads呼び出し)L363
target/vllm/vllm/utils/torch_utils.pyset_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-0EngineCore (mp.Process)Worker, GPUModelRunner(GPU 0)
VllmWorker-1EngineCore (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 Readerrank in handle.local_reader_ranksShmRingBuffer + 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のみ書き込み、実データはZMQ send_multipartで送信
  • Remote Readerへは常にsend_multipartで送信(ShmRingBufferにアクセスできないため)

参照: target/vllm/vllm/distributed/device_communicators/shm_broadcast.py:571-612 (enqueue)

dequeue() のフロー:

  1. acquire_read()でShmRingBufferからチャンクを取得
  2. flag=0(通常): チャンクからbuf_count→各バッファ長→バッファを順次読み出し、pickle.loads(main, buffers=oob_list)でデシリアライズ
  3. 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:

  1. メモリフェンスで最新のメタデータを読む
  2. written_flag=0(未書き込み)または全readerが読み済み(read_count == n_reader)のチャンクを探す
  3. written_flagを0にリセット → データ書き込み → 全readerフラグを0にリセット → メモリフェンスwritten_flagを1に → メモリフェンス
  4. フラグ設定順序が重要: 先にreaderフラグをリセット(case 1維持)→最後にwritten=1(case 2へ遷移)。逆順だとcase 3を経由し、readerが不整合なデータを読む危険

Reader:

  1. メモリフェンスで最新のメタデータを読む
  2. written_flag=1かつ自分のread_flag=0のチャンクを探す
  3. データ読み取り → 自分の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側MessageQueueworker_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 のハンドシェイク

  1. Worker側: __init__内でMessageQueue(1, 1)を生成(writer兼ShmRingBuffer所有者)
  2. Worker側: READYメッセージと共にworker_response_mq.export_handle()をPipe経由でExecutor側に送信
  3. Executor側: wait_for_ready()内でPipeからhandleを受信し、MessageQueue.create_from_handle(handle, 0)でreader側MQを構築
  4. 双方: 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_rankunique_reply_rankパラメータ)を指定できる:

output_rankWorker側の動作Executor側の動作
None全Workerが結果をenqueue全response_mqsからdequeue → リスト返却
0rank 0のみenqueueresponse_mqs[0]のみdequeue → 単一値返却
Nrank Nのみenqueueresponse_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の場合、結果返却が非同期化される:

  1. worker_busy_loop内のhandle_output()async_output_queuequeue.Queue)に出力を投入
  2. 別スレッドasync_output_busy_loop(デーモンスレッド WorkerAsyncOutputCopy)がキューから取り出し
  3. AsyncModelRunnerOutput.get_output()でGPU→CPU非同期コピー完了を待機
  4. 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からの結果取得を遅延評価する:

  1. get_responseクロージャをFutureWrapperに包んで即座に返す
  2. 次回のcollective_rpc呼び出し時に、pending futuresを先にdrainする(futures_queueから順次pop→wait_for_response
  3. 実際に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")
RendezvousTCP(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 なのか

  1. 疎結合: Data Parallelism構成では別ノードに配置される可能性がある。ZMQはネットワーク透過
  2. asyncio統合: Frontendはasyncioイベントループ上で動作し、ZMQのasyncioポーラーと相性がよい
  3. バックグラウンドスレッドでの直列化: msgpackシリアライゼーションをバックグラウンドスレッドで行い、GPU計算とオーバーラップ可能
  4. メッセージ順序保証: ROUTER/DEALERソケットで確定的なメッセージ順序を保証

なぜ EngineCore ↔ Workers は SharedMemory MQ なのか(ZMQではない理由)

  1. 低レイテンシ: 同一ノード内通信に特化。ZMQはネットワークソケット抽象であり、カーネル空間でのバッファコピーとシステムコールのオーバーヘッドがある
  2. ゼロコピー可能: 共有メモリ上でpickleデータを直接読み書きでき、プロセス間のデータコピーが不要
  3. ロックフリー設計: リングバッファ + メタデータフラグ + メモリフェンス(~20ns)で同期。ロック競合なし
  4. collective_rpc最適化: 1対多ブロードキャスト(rpc_broadcast_mq)パターンにリングバッファが最適

なぜ Worker ↔ Worker は NCCL なのか

  1. GPU間テンソル通信専用: NCCLはGPUメモリ間の集合通信(all-reduce等)に特化した高性能ライブラリ
  2. NVLink活用: GPU間直接通信でCPU介在なし。NVLink(最大900GB/s)やPCIe(最大64GB/s)を直接利用
  3. PyTorch統合: モデルコード内のtorch.distributed呼び出しと直接統合
  4. Pythonオブジェクト不可: NCCLはテンソル転送専用であり、Pythonオブジェクト(SchedulerOutput等)の転送には使えない

通信方式比較

通信路方式レイテンシ帯域幅転送対象ネットワーク透過
Frontend ↔ EngineCoreZMQ (TCP)~µsPythonオブジェクト (msgpack)Yes
EngineCore ↔ WorkersSharedMemory MQ~20ns同期Pythonオブジェクト (pickle)No(同一ノード限定)
Worker ↔ WorkerNCCL~µs最高GPUテンソルのみYes(multi-node NCCL対応)

5. TP=1(単一GPU)との比較

TP=1の場合、UniProcExecutorが選択される:

項目TP=1TP=2
ExecutorUniProcExecutorMultiprocExecutor
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.pyAsyncLLM — Frontendプロセスのエントリポイント
target/vllm/vllm/v1/engine/core_client.pyEngineCoreClient — ZMQ通信, CoreEngineProcManager
target/vllm/vllm/v1/engine/core.pyEngineCore, EngineCoreProc — EngineCoreプロセスのエントリポイント
target/vllm/vllm/v1/executor/abstract.pyExecutor — collective_rpc(), execute_model()
target/vllm/vllm/v1/executor/multiproc_executor.pyMultiprocExecutor, WorkerProc — Worker起動, MessageQueue管理, worker_busy_loop
target/vllm/vllm/v1/executor/uniproc_executor.pyUniProcExecutor — 単一GPU用
target/vllm/vllm/v1/worker/gpu_worker.pyWorker — init_device(), torch.distributed初期化
target/vllm/vllm/distributed/device_communicators/shm_broadcast.pyShmRingBuffer, 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):

パラメータ備考
depth27Transformer ブロック数
hidden_size1152内部特徴量次元
num_heads16head_dim = 72
intermediate_size4304MLP中間層
out_hidden_size2048merger出力次元(投影先)
patch_size16ピクセル単位(Gemma3は14)
spatial_merge_size22×2パッチを1トークンに統合
temporal_patch_size2画像は2フレームに複製して入力
num_position_embeddings230448×48 学習済みグリッド
deepstack_visual_indexes[8, 16, 24]中間特徴量抽出レイヤー
in_channels3RGB
hidden_actgelu_pytorch_tanh

Text Model (config.json → text_config):

パラメータ備考
num_hidden_layers48
hidden_size2048
num_attention_heads32
num_key_value_heads4GQA (8:1)
head_dim128
num_experts128
num_experts_per_tok8アクティブエキスパート数
moe_intermediate_size768エキスパートあたり
intermediate_size6144Dense層用
max_position_embeddings262144
rope_theta5,000,000
mrope_section[24, 20, 20]3D M-RoPE
vocab_size151936

特殊トークン:

トークンIDテキスト表現
vision_start151652<|vision_start|>
vision_end151653<|vision_end|>
image_token151655<|image_pad|>
video_token151656<|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)"]

処理順序

  1. OpenAI API受信: image_url付きChatCompletionRequest
  2. 画像取得: MediaConnector.fetch_image() でURL/base64からPILイメージをデコード
  3. チャットテンプレート適用: Jinja2テンプレートで <|vision_start|><|image_pad|><|vision_end|> を挿入
  4. HF Processor呼び出: Qwen3VLProcessorQwen2VLImageProcessorFast でリサイズ・正規化
  5. プレースホルダー展開: <|image_pad|>num_vision_tokens 個の token_id=151655 に置換
  6. 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_classQwen3VLProcessor
image_processor_typeQwen2VLImageProcessorFast
patch_size16
merge_size2
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_pixelsmax_pixels の範囲に収まるようアスペクト比を維持してスケール
  • smart_resizetransformers.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

各ブロック:

  1. LayerNorm (eps=1e-6)
  2. Multi-Head Attention (16 heads, head_dim=72)
  3. Residual connection
  4. LayerNorm
  5. MLP: Linear(1152→4304) → SiLU → Linear(4304→1152)
  6. 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=True
    • norm(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用)

テンソル形式

項目
ndim2 (sanity check: target/vllm/vllm/v1/worker/utils.py:62-89)
shape(num_vision_tokens, 8192)
dtypebfloat16
deviceCUDA GPU(計算時)→ CPU(ECConnector保存時)
keymm_hash (SHA256)
hidden_dim内訳2048 (main) + 2048 (ds_layer8) + 2048 (ds_layer16) + 2048 (ds_layer24)

画像サイズ別テーブル

入力画像resize後grid (t,h,w)num_patchesnum_vision_tokenstensor shapebfloat16 サイズ
256×256256×256(1,16,16)25664(64, 8192)1.0 MB
512×384512×384(1,32,24)768192(192, 8192)3.1 MB
512×512512×512(1,32,32)1024256(256, 8192)4.2 MB
768×768768×768(1,48,48)2304576(576, 8192)9.4 MB
1024×7681024×768(1,64,48)3072768(768, 8192)12.6 MB
1024×10241024×1024(1,64,64)40961024(1024, 8192)16.8 MB
1920×10801920×1088(1,120,68)81602040(2040, 8192)33.4 MB
2048×20482048×2048(1,128,128)163844096(4096, 8192)67.1 MB
4096×30724096×3072(1,256,192)4915212288(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)

テキスト埋め込みへのマージ

  1. main embeddings (N, 2048): _merge_multimodal_embeddings() でplaceholderトークン位置に配置
  2. 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
GateReplicatedLinear(2048, 128)
norm_topk_probtrue

実効計算量: 各トークンで8エキスパート × 768中間 = 6144次元相当(Dense 6144と同等の計算)

パラメータ効率

  • 全パラメータ: ~30B(128エキスパートの重み含む)
  • アクティブパラメータ: ~3B(8/128 = 6.25%のエキスパートのみ活性化)
  • decoder_sparse_step = 1 → 全層がMoE(Dense層なし)

10. Gemma3 27B との比較

項目Gemma3-27B-ITQwen3-VL-30B-A3B
Vision Encoder
アーキテクチャSiglipVisionModelQwen3_VisionTransformer
patch_size1416
hidden_size11521152(同一)
depth2727(同一)
位置埋め込み2D learned (固定)2D learned + bilinear interpolation
RoPEなし2D Partial RoPE (factor=0.5)
投影
方式AvgPool2d(4) + LinearSpatial Merge (2×2) + MLP
出力次元5376 (text hidden)2048
Deepstackなしあり (layers 8,16,24)
Encoder出力dim53768192 (2048×4)
トークン数/画像固定256可変 (64〜12288+)
Temporalなしtemporal_patch_size=2
テキストモデル
アーキテクチャDense TransformerMoE (128 experts)
hidden_size53762048
層数6248
前処理
リサイズ固定896×896smart_resize (32の倍数, 可変)
Pan-and-Scanあり(オプション)なし
正規化ImageNet mean/stdmean=0.5, std=0.5
キャッシュ
encoder_cache tensor(256, 5376) 固定(N, 8192) 可変
キャッシュサイズ/画像2.6 MB 固定1.0〜201+ MB 可変

ECConnector実装への影響

  1. 可変サイズテンソル: Gemma3は固定256トークンだが、Qwen3-VLは画像解像度に依存して大幅に変動(64〜12288+トークン)。ストレージ割り当てに注意が必要
  2. 大きなhidden_dim: 8192次元はGemma3の5376より52%大きい。deepstack情報を含むため圧縮不可
  3. メモリ使用量: 高解像度画像で100MB超のテンソルがありうる。ネットワーク転送コストに注意
  4. 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の使用パターンを体系的に理解し、メッセージ喪失時の挙動を分析する

問い

  1. vLLMはZMQのどのソケットタイプ・通信パターンを使っているか?
  2. ZMQにはネイティブな到達保証やリトライがないが、メッセージが喪失した場合はどうなるか?
  3. vLLM側で信頼性を担保する仕組みはあるか?

ZMQ使用箇所の全体像

vLLM(v1)では16ファイルでZMQが使用されており、以下の5カテゴリに分類できる。

カテゴリ一覧

カテゴリファイル数トランスポート用途
Frontend↔EngineCore通信5IPC / TCPコアのリクエスト/レスポンス通信
DP Coordinator1IPC / TCPData Parallel負荷分散・Wave調整
MessageQueue (ShmRingBuffer)1IPCSharedMemoryオーバーフロー時のフォールバック
KV Cache Events1TCP / IPC外部サービスへのKVイベント配信
KV Transfer コネクタ8TCPノード間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段階ハンドシェイク:

  1. EngineCore→Frontend: DEALER→ROUTERで空メッセージb""送信(ROUTER側がidentityを認識するため必須)
  2. EngineCore→Frontend: "HELLO"メッセージ送信(DP rank、local/remote情報)
  3. Frontend→EngineCore: EngineHandshakeMetadata返却(ZMQアドレス、parallel_config)
  4. 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つの通信チャネル

  1. publish_front (XPUB): Coordinator→Frontend。統計情報(各エンジンのwaiting/running数)とwave状態を配信
  2. output_back (PULL): EngineCore→Coordinator。各エンジンのScheduler統計とwave完了通知
  3. 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
  1. ShmRingBufferのメタデータブロックに overflow=1 フラグを書き込み
  2. 実データはXPUB→SUBソケット経由で送信
  3. 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 MPLookupClient/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→EngineCoresentinelによる即座のクラッシュ検出
ENGINE_CORE_DEAD通知 + lingerEngineCore→Frontend異常終了の明示的通知を保証
ハンドシェイク起動時通信確立の確認後に運用開始
リプレイ機能KV Eventsメッセージドロップ後の回復手段
タイムアウト + リトライKV Transferネットワーク障害時のフォールバック
ベストエフォート + 自己回復DP Coordinator統計は定期再送、waveは再トリガー

結論: vLLMはコア通信パスでは実質的にメッセージ喪失が起きない設計(HWM=0 + IPC + プロセス監視)を採用し、補助的な通信パスではベストエフォート + リカバリ機構(リプレイ、タイムアウト、再トリガー)で対処している。ZMQの「到達保証なし」の弱点は、使用パターンの選択(IPC、HWM=0)とアプリケーション層の監視で効果的に緩和されている。

ソケットタイプ使用一覧

ソケットタイプ使用箇所方向特徴
ROUTERFrontend input, NIXL server, P2P server, Mooncake server, MoRIIO server, KV Events replay, ハンドシェイクbind(サーバー)identityベースルーティング
DEALEREngineCore input, NIXL client, P2P client, Mooncake client, MoRIIO clientconnect(クライアント)透過的なidentity送信
PUSHEngineCore output, EngineCore→Coordinatorconnect単方向、ブロック型
PULLFrontend output, Coordinator←EngineCorebind単方向、フェアキューイング
XPUBCoordinator→Frontend, Coordinator→EngineCore, MessageQueue writerbindサブスクリプション可視化
XSUBEngineCore←Coordinator, Frontend←Coordinatorconnect明示的サブスクリプション
SUBMessageQueue readerconnect自動サブスクリプション
PUBKV Eventsbindブロードキャスト、HWMドロップ
PAIRFrontend内部(shutdown通知、first_req通知)bind/connect排他的1:1ペア
REQNIXL ハンドシェイクclientconnect同期的リクエスト/レスポンス

参照

ファイル内容
target/vllm/vllm/v1/engine/core_client.pyL510-514Frontend側ZMQソケット作成(ROUTER/PULL)
target/vllm/vllm/v1/engine/core_client.pyL539-549ROUTER初期メッセージ待ち(poll + タイムアウト)
target/vllm/vllm/v1/engine/core_client.pyL581-587MessageTracker管理(ゼロコピー参照保持)
target/vllm/vllm/v1/engine/core_client.pyL684-720SyncMPClientの出力処理スレッド(Poller + PAIR shutdown)
target/vllm/vllm/v1/engine/core_client.pyL877-901AsyncMPClientの出力処理タスク
target/vllm/vllm/v1/engine/core_client.pyL1080-1186DPClient統計購読(XSUB + PAIR first_req)
target/vllm/vllm/v1/engine/core.pyL870-920EngineCore側ハンドシェイク(DEALER→ROUTER)
target/vllm/vllm/v1/engine/core.pyL1186-1265EngineCore入力スレッド(DEALER + XSUB + Poller)
target/vllm/vllm/v1/engine/core.pyL1267-1335EngineCore出力スレッド(PUSH, tracker, linger=4000)
target/vllm/vllm/v1/engine/coordinator.pyL113-395DPCoordinator(XPUB×2 + PULL, Wave調整)
target/vllm/vllm/v1/engine/utils.pyL937-1091ハンドシェイクプロトコル(HELLO→metadata→READY)
target/vllm/vllm/utils/network_utils.pyL260-313make_zmq_socket(HWM=0, buf_size, IPv6対応)
target/vllm/vllm/distributed/device_communicators/shm_broadcast.pyL280-403MessageQueue(XPUB/SUB, ShmRingBufferフォールバック)
target/vllm/vllm/distributed/device_communicators/shm_broadcast.pyL571-594enqueue(オーバーフロー判定→ZMQ送信)
target/vllm/vllm/distributed/kv_events.pyL270-400ZmqEventPublisher(PUB + ROUTER replay)
target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/nixl_connector.pyL615-618NIXL ROUTER(RCVTIMEO=1000ms)
target/vllm/vllm/distributed/kv_transfer/kv_connector/v1/p2p/p2p_nccl_engine.pyL124-130P2P NCCL ROUTER/DEALER

関連ドキュメント

用語集

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

全設定を集約するトップレベルクラス。ModelConfigCacheConfigSchedulerConfigParallelConfig等を内包する。

参照: 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での処理:

  1. connector_metadata.requestsを走査し、save_spec.can_saveがTrueのリクエストを処理
  2. skip_leading_tokensをLMCacheのchunk_size(256)の倍数に切り下げてマスク境界を整合
  3. store_maskを構築:プレフィックス部分=False、新規部分=True
  4. LMCacheEngine.store_layer()を呼んでGeneratorを取得、self.layerwise_storersに追加
  5. 最初のリクエストのみ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まで):

  1. TokenDatabase.process_tokens()でトークン列をチャンク分割し、各チャンクのCacheEngineKeyを取得
  2. StorageManager.contains()で既存チャンクをスキップ(layer 0のキーで判定)
  3. StorageManager.batched_allocate()で各チャンク×全レイヤー分のMemoryObjを確保
  4. チャンク×レイヤー → レイヤー×チャンクに転置
  5. 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時):

  1. Paged GPU → 中間GPUバッファ: lmc_ops.single_layer_kv_transfer()(CUDAカーネル)でslot_mappingに基づきscatter→gatherコピー
  2. 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

登録された全ストレージバックエンドにデータを配布するディスパッチャ。

処理フロー:

  1. allocator_backend(通常LocalCPUBackend)の元データをそのまま利用
  2. OrderedDict順に全バックエンド(L1→L2→L3)を走査
  3. 異なるallocatorを持つバックエンドにはallocate_and_copy_objects()で新たにメモリ確保+コピー
  4. 各バックエンドのbatched_submit_put_task()を呼び出し
  5. 最後にref_countをデクリメント

注意: put()メソッドは非推奨RuntimeErrorを投げる)。batched_put()が唯一のエントリポイント。

6. LocalCPUBackend.submit_put_task()

参照: target/LMCache/lmcache/v1/storage_backend/local_cpu_backend.py:141

同期実行(バックグラウンドスレッドなし)。cpu_lock下で以下を実行:

  1. 既存キーの重複チェック
  2. memory_obj.ref_count_up()でrefcount増加
  3. hot_cache[key] = memory_objで保存
  4. cache_policy.update_on_put(key)でEvictionポリシー更新(LRU: OrderedDictの末尾に移動、等)
  5. 必要に応じて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_layerN+1回yield(初期化1回 + レイヤーN回)
  • batched_from_gpuN+1回yield(初期化prime + レイヤーN回)
  • adapterは合計Nnext()を呼ぶ(各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)チャンク単位のキー(レイヤー横断)
LayerCacheEngineKeyCacheEngineKey + layer_idレイヤー単位のキー
MemoryObjpinned CPU tensor wrapperref_count管理、MemoryObjMetadata付き
MemoryFormat.KV_T2D[num_tokens, 2, hidden_dim]レイヤーワイズ形式(token-major)
MemoryFormat.KV_MLA_FMT[num_tokens, hidden_dim]MLA形式(K/V統合)
store_masktorch.Tensor[bool]False=キャッシュ済みprefix、True=新規トークン
slot_mappingtorch.Tensor[long]トークン位置→vLLMページドメモリのflat slot
hot_cacheOrderedDict[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)で通信。

処理フロー:

  1. process_tokens()でトークン列をチャンクハッシュに分割
  2. ハッシュ列をmsgpackシリアライズし、ZMQでLookupServerに送信
  3. LookupServer(Worker側)がStorageManager.contains()で存在チェック
  4. ヒットトークン数を返却

キャッシュ: 同一リクエストの2回目以降のlookupはreqs_status辞書から即座に返す。

Retrieve パスの2モード

モード条件エントリポイント特徴
Bulkuse_layerwise=False(デフォルト)LMCacheEngine.retrieve()全レイヤー一括取得→一括GPU転送
Layerwiseuse_layerwise=TrueLMCacheEngine.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

  1. process_tokens()でチャンク分割・ハッシュ計算
  2. StorageManager.get_block_mapping()でチャンクの所在バックエンドを特定
    • 各バックエンドのbatched_contains()をprefix match方式で呼び出し
    • チャンクを所在バックエンドごとにグルーピング
  3. バックエンドごとにbatched_get()MemoryObjを取得
  4. 取得失敗時はlast_failed_block_start以降のret_maskをFalseに戻し、チャンクリストを切り詰め

_async_process_tokens_internal() の詳細

参照: target/LMCache/lmcache/v1/cache_engine.py:1463

非同期プリフェッチ済みの結果をevent_managerから取得するパス。

  1. event_manager.pop_event(EventType.LOADING, req_id)でprefetch結果のFutureを取得
  2. future.result()list[list[tuple[CacheEngineKey, MemoryObj]]](tier×chunk)を取得
  3. process_tokens()で再度チャンク分割し、memory_obj_mapからマッチングしてチャンクリストを構築
  4. 未使用のMemoryObjref_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する(ヒットあり時):

  1. yield 0: torch.sum(ret_mask) — ヒットトークン数(sglang統合向け)
  2. yield 1 ~ N-1: None — 各レイヤーのGPU転送進行中
  3. yield N: None — 最終レイヤー処理中
  4. yield N+1: next(mem_obj_consumer) で同期
  5. 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

VLLMBufferLayerwiseGPUConnectornum_layers + 2回のイテレーションで3段パイプラインを実行:

イテレーション i操作1: paged書き込み操作2: RoPE補正+gap zeroing操作3: CPU→GPU load
i = 0yieldmem_objs_layer0受領、load_stream上でcopy
i = 1Layer 0のRoPE補正yieldmem_objs_layer1受領、load_stream上でcopy
i = 2Layer 0をpagedメモリに書き込みLayer 1のRoPE補正yieldmem_objs_layer2受領
Layer i-2Layer i-1Layer i
i = NLayer N-2Layer N-1yield(同期用、データなし)
i = N+1Layer N-1

ダブルバッファ: compute_gpu_buffer_objload_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_maskadapter側で構築False=vLLMがキャッシュ済み(チャンク境界まで切り詰め)、True=LMCacheから要ロード
ret_maskEngine内部で構築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を動作
  • LMCacheConnectorV1DynamicLMCacheConnectorV1ImplLMCacheManagerLMCacheEngine

MultiProcess (MP) モード

  • 別プロセスでLMCacheサーバーを起動
  • ZMQ IPCで通信
  • multiprocess/server.pyがメイン
  • 分散StorageManager(distributed/storage_manager.py)を使用

データフロー概要 [SHALLOW]

Store パス(GPU → Storage)

  1. vLLMのforward完了後、wait_for_save()が呼ばれる
  2. GPUConnector.from_gpu() でGPU KVキャッシュ → CPU MemoryObj
  3. TokenDatabaseでトークン列をチャンクキーに変換
  4. StorageManagerが各バックエンドに非同期put

Retrieve パス(Storage → GPU)

  1. Scheduler側でLookupClientがキャッシュ存在確認
  2. Worker側でstart_load_kv()が呼ばれる
  3. StorageManagerがバックエンドからMemoryObjを取得
  4. 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まで):

  1. TokenDatabase.process_tokens(tokens, mask)(start, end, CacheEngineKey)のイテラブル
  2. key.split_layers(num_layers)LayerCacheEngineKeyのリスト
  3. StorageManager.contains(keys[0]) で既存チェック(layer 0のキーで判定)
  4. StorageManager.batched_allocate(shape, dtype, batch_size=num_layers) でMemoryObj確保
  5. チャンク×レイヤー → レイヤー×チャンクに転置
  6. 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_databaseChunkedTokenDatabaseトークン→チャンクハッシュ変換
gpu_connectorGPUConnectorInterfaceGPU↔CPU転送
storage_managerStorageManager多段バックエンド管理
num_layersintモデルのレイヤー数
metadataLMCacheMetadatamodel_name, world_size等
fmtMemoryFormatKV_T2D or KV_2LTD
kv_eventslistBlockStored等のイベントキュー

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のページドメモリに書き戻す。

処理フロー:

  1. _process_tokens_internal()(同期)or _async_process_tokens_internal()(非同期prefetch済み)でMemoryObjを取得
  2. save_only_first_rank時は_broadcast_or_receive_memory_objs()で他ランクにブロードキャスト
  3. GPUConnector.batched_to_gpu(memory_objs, starts, ends, ...)で一括GPU転送
  4. memory_obj.ref_count_down()で解放
  5. remove_after_retrieve時はStorageManager.remove(key)で即座に削除

_process_tokens_internal()(同期パス):

  1. process_tokens()でチャンク分割
  2. get_block_mapping()でチャンクの所在バックエンドをprefix matchで特定
  3. batched_get(keys, location)でバックエンドからMemoryObj取得
  4. 取得失敗時はlast_failed_block_start以降を全て無効化

_async_process_tokens_internal()(非同期パス):

  1. event_manager.pop_event(LOADING, req_id)でprefetch済みFutureを取得
  2. future.result()でtier×chunkのMemoryObjマップを構築
  3. process_tokens()で再チャンク分割しマッチング
  4. 未使用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関数。

初期化フェーズ:

  1. process_tokens()でチャンク分割
  2. StorageManager.contains(layer0_key)でヒット+location統一チェック
  3. キーをlayer-major形式に転置: keys[chunk][layer]keys_layer_major[layer][chunk]
  4. StorageManager.layerwise_batched_get(keys_layer_major, location)get_generator
  5. GPUConnector.batched_to_gpu(starts, ends, ...)mem_obj_consumer Generator

レイヤーループ:

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()でチャンク分割し、StorageManagercontains()/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。

セットアップフェーズ:

  1. slot_mapping_chunksを結合してslot_mapping_fullを構築
  2. use_gpu=True時: gpu_buffer_allocatorから中間GPUバッファを確保

レイヤーループ(各yield間):

ステップuse_gpu=Trueuse_gpu=False
1lmc_ops.single_layer_kv_transfer()
paged GPU → 中間GPUバッファ
lmc_ops.single_layer_kv_transfer()
paged GPU → 直接pinned CPU
2memory_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段パイプライン:

操作ストリーム
LoadCPU pinned → GPUバッファにcopy_(non_blocking=True)load_stream
ComputeRoPE位置補正 + gap zeroingdefault stream
Writelmc_ops.single_layer_kv_transfer()でバッファ→pagedメモリdefault stream

ダブルバッファ: compute_gpu_buffer_objload_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(インターフェース定義)

サブドキュメント:

StorageManager

バックエンド登録と優先度

storage_backendsOrderedDictで登録順=優先度:

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

処理フロー:

  1. allocator_backendのデータをそのまま利用(コピー不要)
  2. OrderedDict順に全バックエンド(L1→L2→L3)を走査
  3. 異なるallocatorを持つバックエンドにはallocate_and_copy_objects()で新メモリ確保+コピー
    • 実際にはLocalDiskBackendもRemoteBackendもget_allocator_backend()→LocalCPUBackendなので、同一allocator=コピー不要
  4. 各バックエンドのbatched_submit_put_task()を呼び出し
  5. 全バックエンド処理後、各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下で:

  1. 重複チェック: key in hot_cache → スキップ
  2. memory_obj.ref_count_up()
  3. hot_cache[key] = memory_obj
  4. cache_policy.update_on_put(key) — Evictionポリシー更新
  5. batched_msg_sender.add_kv_op(ADMIT, key.chunk_hash) — controller通知(オプション)
  6. ロック外で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で呼ばれると:

  1. hot_cache[key].pin() → Eviction対象外にマーク
  2. 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等)
  • 下流:
  • 依存: 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

  1. 重複チェック: exists_in_put_tasks(key) → スキップ
  2. disk_worker.insert_put_task(key) — 進行中タスクリストに登録
  3. ディスク容量Eviction: disk_lock下で current_cache_size + required_size > max_cache_size の間、 cache_policy.get_evict_candidates()batched_remove() + os.remove(path)
  4. memory_obj.ref_count_up() — 非同期書き込み中の保護
  5. 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のスレッドプール)。

  1. memory_obj.byte_arraywrite_file(buffer, path)
  2. memory_obj.ref_count_down() — 参照解放
  3. insert_key(key, size, shape, dtype, fmt)self.dictにDiskCacheMetadata登録
  4. disk_worker.remove_put_task(key)
  5. 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

  1. disk_lock下でself.dict[key]からpath/shape/dtype/fmt取得
  2. cache_policy.update_on_hit(key, self.dict) — アクセス順更新
  3. local_cpu_backend.allocate(shape, dtype, fmt)CPUメモリに確保
  4. read_file(key, buffer, path) — ファイルからバッファに読み込み
  5. metadata.cached_positionsをDiskCacheMetadataから復元

batched_get_non_blocking() — 非同期プリフェッチ

参照: target/LMCache/lmcache/v1/storage_backend/local_disk_backend.py:410

  1. 各キーについて:
    • local_cpu_backend.allocate() でCPUメモリ確保
    • self.dict[key].pin() — 読み込み中のEviction防止
    • cache_policy.update_on_hit() — アクセス順更新
  2. 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

タスク種別優先度説明
prefetch0 (最高)ディスク→CPUメモリ読み込み
delete1ファイル削除
put2 (最低)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等のアクセス順更新が必要な場合

メモリ確保パターン

独自バックエンドからデータを取得する場合:

  1. local_cpu_backend.allocate(shape, dtype, fmt) でCPUメモリを確保
  2. データをMemoryObjのbyte_arrayに書き込み(readinto()等)
  3. metadata.cached_positions等のメタデータを復元
  4. 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_size1つの大きなブロックとして確保し、 torch.chunk()で分割。これによりフラグメンテーションを軽減。

free() / batched_free()

  • free(): AddressManager.free()でブロックを返却→前後のFreeBlockとcoalesce
  • batched_free(): メモリブロックをアドレス順にソートし、隣接ブロックを事前にcoalesceしてから AddressManager.free()を呼ぶ。フリーリスト操作回数を削減

PagedTensorMemoryAllocator — ページスロット方式

参照: target/LMCache/lmcache/v1/memory_management.py:1404

固定サイズのスロット(ページ)に分割し、deque[TensorMemoryObj]でフリーリストを管理。

  • 初期化: bufferalign_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)
  • 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つの入力モード:

  1. tokens入力: トークン列を受け取り、チャンク分割→ハッシュ計算
  2. hashes入力: 事前計算済みハッシュ+offsetsを受け取り、キー生成のみ

チャンク分割アルゴリズム(tokens入力時):

  1. _chunk_tokens(): chunk_size(デフォルト256)単位で分割
    • save_unfull_chunk=True(デフォルト): 端数チャンクも保存
    • save_unfull_chunk=False: 端数は切り捨て
  2. _prefix_hash(): プレフィックスチェーンハッシュを計算
    • 初期値: NONE_HASH(vLLMから取得、kv_cache_utils.init_none_hash()
    • 各チャンク: hash_func((previous_hash, token_tuple, extra_keys))
  3. 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_64bitsha256_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の処理:

  1. connector_metadata.requestsからsave_spec.can_save=Trueのリクエストを抽出
  2. skip_leading_tokensをchunk_size(256)の倍数に切り下げてマスク整合
  3. store_maskを構築:prefix部分=False、新規部分=True
  4. LMCacheEngine.store_layer()でGenerator生成、self.layerwise_storersに追加
  5. 最初のリクエストのみsync=True(CUDAストリーム同期)

全レイヤー共通: layerwise_storers内の全Generatorをnext()で1ステップ進行。

ConnectorMetadata

参照: target/LMCache/lmcache/integration/vllm/vllm_v1_adapter.py (LMCacheConnectorMetadata)

Scheduler側で構築され、Worker側に渡される。各リクエストのtoken_idsslot_mappingsave_speccan_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キャッシュのヒット数を返す。

  1. LookupClient.lookup_cache(req_id)で既存キャッシュ確認(2回目以降)
  2. 未キャッシュならLookupClient.lookup(token_ids, req_id)でZMQ経由でWorker側に問い合わせ
  3. LoadSpec(vllm_cached_tokens, lmcache_cached_tokens, can_load=False)を生成
  4. update_state_after_alloc()can_load=Trueに更新(ブロック確保成功時)
  5. 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の構築:

  1. request.load_spec.vllm_cached_tokensをchunk_sizeの倍数に切り下げ → masked_token_count
  2. token_mask[:masked_token_count] = False(vLLM既キャッシュ分)、残り=True

2モード分岐:

  • Layerwise (use_layerwise=True):
    1. LMCacheEngine.retrieve_layer()でGenerator取得
    2. next() × 2回で先行2レイヤー分をキック
    3. self.layerwise_retrieversにGenerator追加
  • Bulk (use_layerwise=False):
    1. LMCacheEngine.retrieve()を呼び出し、ret_maskを取得
    2. 取得失敗時は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, ...

アルゴリズム:

  1. キャッシュから取得した old_k と新たに計算した k の差分L2ノルムを計算
  2. 差分が大きい(= キャッシュが不正確)tokenを recomp_ratios 割合だけtopk選択
  3. 重要tokenのみQ/K/Vを保持して再計算(他はキャッシュ値を使用)
  4. 最終的に完全な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種のみ):

  • LlamaForCausalLMLMCLlamaModel
  • Qwen2ForCausalLMLMCLlamaModel(同実装)
  • Qwen3ForCausalLMLMCQwen3Model

参照: 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_idENGINE_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.pyload_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_cacheGPUバッファ(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_BLENDINGenable_blendingFalseCacheBlend有効化
LMCACHE_BLEND_SPECIAL_STRblend_special_str" # # "セパレータ文字列
LMCACHE_BLEND_CHECK_LAYERSblend_check_layersNone重要token判定レイヤー(カンマ区切りリスト)
LMCACHE_BLEND_RECOMPUTE_RATIOSblend_recompute_ratiosNone再計算割合(カンマ区切りfloatリスト)
LMCACHE_BLEND_THRESHOLDSblend_thresholdsNone重要token判定閾値(未使用/TODO)
LMCACHE_BLEND_MIN_TOKENSblend_min_tokens256Blend対象の最小トークン数
LMCACHE_USE_LAYERWISEuse_layerwiseFalseレイヤーワイズ転送(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)

コアコンセプト

用語説明
LMCacheEngineKVキャッシュのstore/retrieve/prefetchを統合するメインエンジン。lmcache/v1/cache_engine.py
LMCacheManagerLMCacheの内部コンポーネント(Engine, LookupClient, OffloadServer等)のライフサイクル管理。lmcache/v1/manager.py
CacheEngineKeyKVキャッシュチャンクの一意識別子。(model_name, world_size, worker_id, chunk_hash, dtype, request_configs)の6タプル。lmcache/utils.py:333
LayerCacheEngineKeyCacheEngineKey + layer_id。レイヤー単位保存時のキー。split_layers()で生成。lmcache/utils.py:392
MemoryObjKVキャッシュデータを保持する抽象メモリオブジェクト。フォーマット情報とテンソルデータを包含。lmcache/v1/memory_management.py
MemoryFormatKVキャッシュのメモリレイアウト種別。KV_2LTD, KV_T2D, KV_2TD, BINARY, KV_MLA_FMT等。
LMCacheMetadataモデル名、world_size、worker_id、kv_dtype、kv_shape等のメタ情報。サービングエンジンから抽出。
LMCacheEngineConfigYAML/環境変数ベースの設定。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_maskstore時のマスク。False=already-cached prefix、True=新規トークン。False数はchunk_sizeの倍数必須。

ストレージ

用語説明
StorageManager複数のストレージバックエンドを階層管理。put/get要求を各バックエンドに振り分け。
StorageBackendInterfaceストレージバックエンドの抽象インターフェース。contains/put/get等。
LocalCPUBackendCPU メモリ上のKVキャッシュストレージ(L1)。hot_cache(OrderedDict)で管理。同期書き込み。
hot_cacheLocalCPUBackendのOrderedDict[CacheEngineKey, MemoryObj]。CachePolicyでEviction管理。
allocator_backendMemoryObj確保を担当するバックエンド。通常はLocalCPUBackend。
LocalDiskBackendディスク上のKVキャッシュストレージ(L2)。
RemoteBackendリモートストレージ(L3)。connector経由でRedis/S3/Valkey等に接続。
P2PBackendインスタンス間のP2P KVキャッシュ転送。
NIXLBackendNVIDIA NIXL経由の高速転送。
GdsBackendGPUDirect Storage経由の転送。
CachePolicyEviction方針。FIFO/LRU/LFU/MRUから選択可能。
Serdeシリアライゼーション/デシリアライゼーション。naive(無圧縮)、CacheGen(圧縮)、KIVI等。

GPU連携

用語説明
GPUConnectorInterfaceGPU KVキャッシュとCPU MemoryObj間のデータ転送抽象インターフェース。to_gpu/from_gpu。
VLLMPagedMemGPUConnectorV2vLLMのPaged KVキャッシュ向けGPUコネクタ(非レイヤーワイズ)。全レイヤー一括転送。
VLLMPagedMemLayerwiseGPUConnectorレイヤー単位でKVキャッシュを転送するコネクタ。ジェネレータパターン使用。主要パス。
lmc_ops.single_layer_kv_transferCUDAカーネル。vLLMのページドKVキャッシュからslot_mapping経由でデータを抽出/書き戻し。
slot_mappingトークン位置→vLLMページドメモリのflat slot位置へのマッピング。GPU Tensor。
store_streamGPU→CPU転送専用CUDAストリーム。メイン計算ストリームとオーバーラップ可能。
load_streamCPU→GPU転送専用CUDAストリーム。retrieve時にメイン計算とオーバーラップ。
lmc_ops.multi_layer_kv_transferCUDAカーネル。全レイヤー一括でMemoryObj→paged KVキャッシュに転送(Bulk retrieve用)。
fused_rotary_embRoPE位置補正関数。Layerwise retrieve時に保存時と現在のposition差分を補正。
VLLMBufferLayerwiseGPUConnectorCacheBlend対応のLayerwiseコネクタ。ダブルバッファ+RoPE補正+gap zeroing。

統合

用語説明
LMCacheConnectorV1DynamicvLLMのKVConnectorBase_V1実装。LMCacheConnectorV1Implに委譲。
LMCacheConnectorV1ImplvLLM統合の実装本体(vllm_v1_adapter.py)。LoadSpec/SaveSpecでロード・保存を管理。
LoadSpecロード仕様。vLLMキャッシュ済みトークン数、LMCacheキャッシュ済みトークン数、ロード可否。
SaveSpec保存仕様。skip_leading_tokens(キャッシュ済みプレフィックス長)、can_save(保存可否)。
ConnectorMetadataScheduler→Worker間で渡されるメタデータ。各リクエストのtoken_ids, slot_mapping, LoadSpec, SaveSpecを含む。
kv_role"kv_both"(default)/"kv_producer"/"kv_consumer"。producer時はskip_leading_tokens=0。
LookupClientScheduler側でキャッシュ存在確認を行うZMQベースクライアント。lmcache_lookup_client.py
LookupServerWorker側でLookupClientからのZMQ REQ/REPを受け付け、StorageManager.async_lookup_and_prefetchを実行。
EventManager非同期イベント(LOADING等)のFutureを管理。lookup_idでprefetch結果とretrieve消費を紐付け。
token_maskretrieve時のマスク。False=vLLMがキャッシュ済み(chunk_sizeの倍数に切り下げ)、True=LMCacheからロード対象。
ret_maskretrieve結果のマスク。True=LMCacheから実際に取得成功、False=未取得。Engine内部で構築。
write-backStorageManager.batched_get()がリモートバックエンドから取得した場合、自動的にLocalCPUBackendにコピーする動作。
get_block_mappingチャンクの所在バックエンドをprefix match方式で特定するStorageManagerメソッド。

CacheBlend

用語説明
CacheBlend非プレフィックス部分のKVキャッシュも再利用する技術。重要トークンを再計算して品質保持。
BlenderCacheBlendのblending計算を実行するコンポーネント。lmcache/v1/compute/blend/
blend_recompute_ratios再計算するトークンの割合。
blend_special_strセグメント分割用セパレータ文字列。デフォルト" # # "

分散・マルチプロセス

用語説明
CacheController複数LMCacheインスタンス間のキャッシュ状態を中央管理するコントローラ。
LMCacheWorkerCacheControllerと通信するワーカー。Heartbeat/Register/P2P Lookup。
MultiProcess ServerZMQ IPCベースの別プロセスLMCacheサーバー。SessionManager, GPUCacheContext管理。
BlendServerCacheBlend用MPサーバー。MPCacheEngine継承。
OffloadServerKVキャッシュオフロード用ZMQサーバー。
Disaggregated Prefill (PD)Prefill/Decode分離アーキテクチャ。NIXL経由でPD間転送。

設定キー(主要)

設定名デフォルト説明
chunk_size256チャンクのトークン数
local_cputrueCPU バックエンド有効化
max_local_cpu_size5.0 (GB)CPUストレージ上限
local_diskNoneディスクパス(Noneで無効)
remote_urlNoneリモートストレージURL
remote_serde“naive”リモート用Serde
use_layerwisefalseレイヤー単位転送
enable_blendingfalseCacheBlend有効化
enable_p2pfalseP2P転送有効化
enable_pdfalseDisaggregated Prefill
enable_controllerfalseCacheController有効化
save_decode_cachefalseDecodeフェーズのキャッシュも保存