はじめに — 1人で使う分には見えない話
これまで手元の RTX 4070(12GB)では「どうすれば速くなるか」を中心に書いてきました。今度は、そのローカル LLM を、何人か・複数のアプリで共有し始めたときの話です。
1 人で使ううちは気づきませんが、1 台を共有するとサーバの中に「共有される状態」ができます。正体は KV Cache(モデルが計算した途中結果を使い回すためのメモリ)です。とくに vLLM の Prefix Cache(入力の先頭が前と同じなら、その部分の計算を使い回す仕組み)は、何もしないと誰のリクエストでも同じ置き場を共有します。速くはなりますが、「誰と誰のキャッシュが一緒になるのか」を決めておかないと危うい。
共有される範囲は、リクエストに短い文字列を一つ添えるだけで分けられます。vLLM ではこの文字列を cache_salt(以下、合言葉) と呼び、同じ合言葉の入力どうしはキャッシュを使い回して速くなり、違う合言葉なら文章がまったく同じでも別物として扱うことができます。
要点
異なる利用者で vLLM を共有する場面を想定し、合言葉なしだと、同じ文章のキャッシュが利用者をまたいでどこまで使い回されるのかを確かめてみました。
- 同じ文章を vLLM に 2 回 送る → 2 回目だけ数倍速くなった(前回の計算が使い回された)。
- 同じ文章に、違う 合言葉(
cache_salt)を付けて送る → 速くならず、1 回目と同じ時間に戻った(合言葉でキャッシュが分かれた)。
わかったことは 1 つです。合言葉なしで共有すると、同じ文章は「誰が送っても同じキャッシュ」になる。 そして合言葉を変えれば、その共有はきれいに分けられる。だから複数人へ公開するなら、「キャッシュをどの単位で分けるか」を運用側で決める必要がある——これが問題編の結論です。
確認したのは「利用者ごとの分離が効いていない」という一点です。「他人のプロンプトが読める(情報の混入)」こととは別の話で、そちらは本記事では測っていません。
対象読者
- ローカル LLM を個人利用から社内共有・チーム共有へ広げたい人
- vLLM / SGLang / Ollama などで推論サーバを立てている人
- 「外部 API に投げないから安全」を運用の根拠にしている人
KV Cache や Prefix Cache そのものの一般解説が主題ではありません。KV Cache の容量・長文の話は既刊「FP8 KV Cache でより長い長文を処理できるか」で扱っています。
検証環境
自分の RTX 4070(12GB)1 枚で、専用の vLLM を 1 つ立てて測りました。
- ハードウェア: GeForce RTX 4070 12GB / NVIDIA driver 580.126.20 / Ubuntu 24.04
- 推論エンジン: vLLM 0.21.1rc1.dev243+ga5bbd81e2(コンテナ vllm/vllm-openai:nightly / OpenAI 互換 API)
# nightly タグは動くので、実際に使ったイメージは digest で固定できる:
# sha256:484204545cf7e232b8d7df6d4bb721e667a12b84dd67bbaf84b322868d16cb30
- CUDA / PyTorch: CUDA 13.0 / PyTorch 2.11.0(いずれもコンテナ同梱。ホストの nvcc とは無関係)
- モデル: RedHatAI/Qwen3.5-4B-FP8-dynamic(FP8 量子化。BF16 は 12GB で OOM のため FP8)
- 主な起動フラグ:
--enable-prefix-caching # 本題。Prefix Cache を有効化
--enforce-eager # CUDA graph は 12GB では OOM しやすいので無効化
--max-model-len 32768 # 全文コンテキスト+余裕
--gpu-memory-utilization 0.90 # 32K KV キャッシュ確保のため 0.93 より下げた
- 測定条件: temperature 0 / max_tokens 4 / 各条件 10 試行。各試行は先頭に一意な trial-marker を付け、前試行とプレフィックスを一致させないことで毎回の cold を担保
- prompt: 合成した架空文書 2 種(中くらいの長さ / 長め)。実在のデータではない
max_tokens を 4 と極端に小さくしているのは、文章の生成そのものではなく「入力を処理し終えて最初の返事が返り始めるまで」を見たいからです。Prefix Cache が効くのはこの入力処理の部分なので、生成を最小にして差を素直に観測します。
仕組みは 2 つだけ押さえれば十分
Prefix Cache は、同じ入力の先頭を再び受け取ったとき、その部分の計算をやり直さず前回の結果を使い回す仕組みです。使い回せた(ヒットした)ぶん反応が速くなり、vLLM の内部カウンタでは「使い回したトークン数」が増えます。見るのはこの 2 つ——反応時間(TTFT=最初の 1 トークンが返るまでの時間) と 使い回したトークン数——です。
もう 1 つが cache_salt(合言葉) です。リクエストに付けられる短い文字列で、vLLM は「同じ入力か」を判定する材料にこの合言葉も混ぜます。これはすでに vLLM に実装済みの正式機能で、公式ドキュメントの "Cache Isolation" に載っています。つまり——
- 合言葉なし → 文章が同じなら、誰が送っても同じキャッシュを共有する。
- 合言葉が違う → 文章がまったく同じでも、別物として扱いキャッシュを共有しない。
図にすると、こうです。同じ文章でも、合言葉があるかどうかで「棚」の分かれ方が変わります。
{
"model": "kvsec-qwen35-4b",
"messages": [{ "role": "user", "content": "..." }],
"cache_salt": "workspace-42"
}
合言葉が無いとき、キャッシュの共有範囲はどこまで広がるか。ここが問題編の焦点です。
中で何が起きているか(もう少しだけ技術的な話)
① Prefix Cache の実体:ブロックのハッシュを鎖でつなぐ
vLLM は KV Cache を一定サイズのブロックに区切り、各ブロックに「そこまでのトークン列から計算したハッシュ(内容の指紋)」を付けます。ハッシュは前のブロックから引き継いで鎖状につながります。
- 先頭ブロックのハッシュが既存と一致 → その計算をスキップして使い回す(これがヒット)。
- 「先頭からどこまで前と同じか」をブロック単位で判定できる。
② cache_salt が効く理由:鎖の起点をずらす
合言葉は上図のとおり先頭ブロックのハッシュの材料に混ざります。起点が変わると先頭ブロックのハッシュが変わり、鎖でつながる後続もまとめてずれます。同じ文章(同じトークン列)でも、合言葉 a と b では別のハッシュ列になり、一致しません。
- 合言葉が違う → 文章が同じでもハッシュが一致せず、使い回されない。
- 合言葉なし(=同じ既定値) → 同じ文章は同じハッシュ → 誰のリクエストでも一致。「合言葉なしだと全員で共有」はこの素直な帰結。
③ 出典(実装済みの正式機能)
| 区分 | 内容 |
|---|---|
| 提案 | RFC #16016(タイミング差からの推測 = side-channel 対策として提起) |
| 実装 | PR #17045(2025-04-30 マージ) |
| 有効範囲 | 新エンジン(V1)。OpenAI 互換クライアントからは extra_body で渡す |
今回の実測もこの実装済み機能を使っています。
④ SGLang も同じ発想
SGLang は共通の先頭を木構造(RadixTree)でまとめて再利用します。鍵をリクエスト側から分けられる点も、「先頭が一致する限り使い回す」前提も共通です。だから共有の区切りを設計するという話はエンジンをまたいで当てはまります。
結果
同じ prompt について、次の 3 つを測りました。①合言葉なしで 2 回、②同じ合言葉で 2 回、③同じ文章に違う合言葉。反応時間は 10 試行の中央値、使い回したトークン数は 10 試行の合計です。
中くらいの長さの prompt
| 条件 | 反応時間(TTFT) | 使い回したトークン |
|---|---|---|
| 合言葉なし・1 回目 | 274.9ms | 0 |
| 合言葉なし・2 回目 | 70.4ms | 15,840 |
| 同じ合言葉・2 回目 | 68.6ms | 15,840 |
| 同じ文章+違う合言葉 | 273.4ms | 0 |
長めの prompt
| 条件 | 反応時間(TTFT) | 使い回したトークン |
|---|---|---|
| 合言葉なし・1 回目 | 500.8ms | 0 |
| 合言葉なし・2 回目 | 73.8ms | 31,680 |
| 同じ合言葉・2 回目 | 74.6ms | 31,680 |
| 同じ文章+違う合言葉 | 499.7ms | 0 |
読み取りは 2 つだけです。
- 2 回目は速い。 合言葉なしでも同じ合言葉でも、2 回目は反応時間が数倍縮み(274.9→70.4ms、500.8→73.8ms)、裏では使い回したトークンが 0 から 15,840 / 31,680 に増えています。「速くなった」は反応時間だけの錯覚ではなく、使い回しカウンタでも裏が取れています。
- 合言葉が違えば速くならない。 文章がまったく同じでも、違う合言葉を付けると使い回しは 0 に戻り、反応時間も 1 回目と同じに戻りました。合言葉でキャッシュを分けられる、が確認できました。
何が「危ない」のか
ここが本題です。合言葉を付けずに vLLM をそのまま共有 API として公開すると、同じ文章は誰が送っても同じキャッシュに入ります。今回測れたのは、まさにその状態です。利用者や案件の区切りとは無関係に、同じ文章が同じキャッシュとして扱われている。
これがなぜ運用上まずいか、理由は 2 つです。
- 分けたい単位で分かれていない。 「利用者 A」「案件 B」のように分けたい区切りで、キャッシュが分かれていません。とくに共通の system prompt や、同じ社内文書を使う RAG のように、みんなの入力の先頭が一致しやすい場面では、別々の人のリクエストが同じキャッシュ経路に乗り得ます。
- 速い・遅いが外から見える。 今回のとおり、使い回しが起きると反応時間が数倍変わります。つまり返事の速さの差から「この文章は、すでに誰かが送った」と推測され得る。これはマルチテナント環境で KV Cache 共有が論点になる、という研究(NDSS 2025 ほか)でも中心的な指摘です。
確認できたのは**「キャッシュが利用者ごとに分かれていない」という状態までで、他人のプロンプト本文や回答がそのまま読める、という情報の混入を実証したわけではありません**。「混入が起きた」のではなく、「混入の可能性を上げる状態にある」ということです。
言い換えると、合言葉なしの共有公開は、速くするつもりの Prefix Cache が、設計されないまま共有状態になっているということです。速さは本物ですが、その速さが誰と誰の間で共有されているかを運用側が把握していません。
考察 — わかったこと・限界
わかったこと
- 今回のローカル vLLM 環境では、合言葉なしでも同じ文章の再送で Prefix Cache がヒットし、反応時間が数倍縮んだ。
- 同じ文章でも違う合言葉を付けると使い回しは 0 に戻り、反応時間も 1 回目相当になった。合言葉で共有範囲を分けられる。
- だから共有サーバでは、全員で共有してよいのか、利用者・案件ごとに分けるのかを運用設計として決める必要がある。
限界
- これは RTX 4070・Qwen3.5-4B-FP8・vLLM nightly・合成 prompt・10 試行という特定条件の実測です。すべてのモデル・バージョン・負荷で同じ数値になるとは限りません。
- 使い回しは入力の先頭が一定サイズ単位で一致したときに起きます。現実の利用者どうしが完全に同じ文章を送ることは多くありませんが、共通テンプレート・共通 system prompt・同じ RAG 文書では一致しやすい、が実務上の勘所です。
まとめ
- ローカル LLM を共有 API 化すると、Prefix Cache は「高速化の仕組み」であると同時に「共有される内部状態」になる。
- 合言葉なしでは、同じ文章が利用者・案件の区切りと無関係に同じキャッシュへ入り、反応時間の差としても観測できた(約 3.9〜6.8 倍速・使い回し 15,840〜31,680 トークン)。
- 合言葉を変えれば共有範囲は分かれる。だから対策は「Prefix Cache を切る」ではなく「どの単位で分けるかを設計する」。
次回(対策編)は、これを運用へ落とします。利用者に合言葉を付けさせるのではなく、vLLM を直接公開せず、前段に置いた窓口(LLM Gateway / API Proxy)が、ログイン情報から合言葉を自動で付ける設計を示し、「分けても、同じグループ内では速さが残る」ことを同じ環境で実測します。
役に立ったら LGTM を、読み返す方はストックが便利です(更新時に通知されます)。設計上の見落としや間違いがあればコメントで指摘してください。追記します。
実験に使ったローカルベンチのリポジトリ: https://github.com/ai-systems-notes/local-llm-rag-cag-benchmark
参考文献
- vLLM, "Automatic Prefix Caching"(先頭の使い回しの仕組みと、
cache_saltによる Cache Isolation。正式機能) - vLLM PR #17045, "[Core] Prevent side-channel attacks via cache salting"(
cache_saltの実装。2025-04-30 マージ) - vLLM RFC #16016, "Cache Salting for Secure and Flexible Prefix Caching in vLLM"(
cache_saltの提案・議論。closed) - NDSS 2025, "I Know What You Asked: Prompt Leakage via KV-Cache Sharing in Multi-Tenant LLM Serving"
- NVIDIA, "Structuring Applications to Secure the KV Cache"
- Acompany, "KV Cache の仕組みと最適化手法"(日本語)
- 既刊:FP8 KV Cache でより長い長文を処理できるか
