はじめに
vLLM の prefillキャッシュは、GPU から溢れると捨てられます。捨てる代わりに CPU の RAM へ退避しておくと、次に同じ前置きが来たとき 254 ms で戻せました。全部計算し直せば 602 ms、GPU に残っていれば 230 ms なので、外に出しても足すのは 24 ms です。
増設するのは VRAM ではなく、桁違いに安い RAM で済みます。
ただし最後まで通ったのは dense の Qwen3-4B のほうで、ハイブリッドの Qwen3.5-4B は戻す経路が今日の vLLM ではまだ開いていません。RTX 4070 12GB で両方を実測し、ソースまで辿った記録です。
(しばらくは前提の整理が続きます。前シリーズを読んだ方は「要点」まで飛ばしてください。)
前のシリーズでやったのは、RAG と CAG の直接対決でした。自作小説(約 2,450 トークン)を題材に、同じ 50 問・同じ採点者で、知識の渡し方だけを変えます。
RAG は検索で引いてきたチャンクだけを渡し、CAG は全文をまるごとプロンプトの先頭に固定して渡す。結果は 2.00 点満点で RAG 1.58 / CAG 1.90、不正解(0 点)は RAG 7 問に対して CAG は 1 問でした。しかもプロンプト長は CAG が 6.4 倍(393 → 2,529 トークン)なのに、TTFT(最初の 1 トークンが返るまでの時間)は CAG のほうが速い(0.099 → 0.088 秒)。
2 問目以降は、貼り付けた全文ぶんの計算を丸ごと使い回せるからです(実装編 / 結果編)。
そこから出した提言が、外したら困る知識は検索に賭けず、プロンプトの先頭に固定してしまうでした(検索が外す瞬間そのものは別記事で解剖しています)。
固定した長い資料を手元の GPU キャッシュに置いて、同じ資料に何度も質問する——今回は、その運用を RTX 4070 の 12GB で実際に回します。
ただし、前回のままの構成では何も起きません。小説 1 本ぶんの約 2,450 トークンは、この構成で確保される K/V の置き場(後述。70,284 トークンぶんでした)の 3.5% しか使わないからです。
詰まり始めるのは、これを運用に載せたときです。資料を長くする/資料の本数を増やす/同時に使う人が増える——どれをやっても、性質の違う 2 つの上限に当たります。
- 資料をどこまで長くできるか — 1 リクエストに載せられる長さの上限
- 2 回目以降、その資料の計算をどこまで省けるか — 前置きの再計算を避けられる範囲
1 本を長くすると 1 に、本数や人が増えると 2 に効いてきます。この 2 つは別々の仕組みが担当していて、しかも名前が似ています。混ぜると以降の話が通じなくなるので、先に分けます。
上限を決めている 2 つのキャッシュ
推論は 2 段階です。前半が prefill で、貼り付けた文章を一気に通し、各トークンの K と V(アテンションが参照する値)を計算します。後半がデコードで、1 トークン生成するたびに過去の K/V を読み、自分の K/V を書き足していきます。
-
KV キャッシュ:デコードが過去の K/V を読むために GPU 上に置いておくもの。1 リクエストの中だけ生きていて、終われば解放されます。
- 上限 1(LLMで扱う読み書きの長さ)を決めているのはこれです
-
prefillキャッシュ:prefill の計算結果をリクエスト終了後も捨てずに残し、先頭が一致する次のリクエストで使い回す仕組み。リクエストをまたいで生きます。
- 上限 2(2 回目以降を速くできるか)を決めているのはこれです
vLLMでのprefillキャッシュ再利用設定について
vLLM での機能名は automatic prefix caching で、有効化フラグは --enable-prefix-caching、メトリクスも vllm:prefix_cache_hits_total です。先頭からブロック単位でハッシュ照合して再利用するので「prefix」と呼ばれますが、再利用しているのは prefill の計算結果です。本記事では仕組みを指すときは「prefillキャッシュ」、フラグやメトリクスの名前を出すときは実際の識別子をそのまま書きます。
2 つは同じ KV プールを分け合っている
K/V を置くメモリは、必要になるたびに都度確保されるわけではありません。vLLM は起動時に、VRAM の空きから一定量を K/V 置き場としてまとめて押さえます(取り分は --gpu-memory-utilization が決めます)。
以後、K/V はすべてこの 1 つの置き場から貸し出されます。本記事ではこれを KV プールと呼びます。本構成での実測は 70,284 トークンぶんでした。
貸出先は 2 種類あります。
- いま生成中のリクエストが握っているブロック(= KV キャッシュ)
- 使い終わったが、次に同じ前置きが来たときのために取っておくブロック(= prefillキャッシュ)
つまりこの 2 つは、同じ 70,284 トークンぶんを分け合っています。資料を長くすれば、取っておけるぶんが押し出される。片方だけ見て調整できないのは、このためです。
そして API 経由で LLM を使っているあいだ、この KV プールは一切見えません。どれだけの大きさで、何を残して何を捨てるかは、すべて提供側が決めています。
自分の GPU で動かして初めて、上のような数字として目の前に出てきます。私の場合はその元手が VRAM 12GB です。
私の努力が伝わらない悲しいところ
自分でLLMをサーブしていて少し悲しいのは、こうした調整の多くが利用者からは見えないことです。モデルをVRAMに収め、長いコンテキストを扱い、複数人が同時に使え、2回目以降はキャッシュで速くする。そのために量子化方式やKV容量、並列数、キャッシュ設定を一つずつ決めています。
でも使う側からすれば、「長い文章が入る」「複数人で使える」「速く返る」は最初からできることとして、その上にエージェントや検索などの便利さを積み上げて評価します。うまく調整できているほど、その存在は意識されません。サービングは、成功すると努力が見えなくなる仕事なのかもしれません。
上限ごとに、効くアプローチが違う
この記事が扱うのは、2 つの上限に対するアプローチ 1 つずつです。押し上げ方はほかにもあります——重みをもっと強く量子化する、KV プールの取り分(--gpu-memory-utilization)を上げる、渡す資料そのものを削る。今回試したのは、別のモデルに乗り換えず、資料も削らずに、K/V の持ち方と置き場所だけを変える 2 つです。
使うモデルは 2 つです。Qwen3.5-4B は、層の大半が線形アテンション——トークンごとの K/V を積み上げる代わりに、そこまでを畳み込んだ状態を 1 個だけ持ち回る方式で、残りの層が通常のフルアテンションというハイブリッド構成です。
対照用の Qwen3-4B は全層がフルアテンションで、本記事ではこちらを dense と呼びます(MoE の対語としてではなく、アテンション構造の話です)。配布元もパラメータ規模も同じ、重みはどちらも FP8 に量子化済み。狙って変えたのは、アテンションの構造です。
アプローチ 1:重みではなく「K/V のほう」を小さくして、資料を長くする余地を作る(上限 1 に効く)。 K/V を 8bit(FP8)で持つようにすると、同じ VRAM で確保できる KV プールが 1.82 倍になりました。回答の精度はほぼ変わりません(FP8 KV そのものの単体検証は前回記事にあります)。プールが広がれば、1 リクエストに載せられる長さにも、次に備えて取っておける prefillキャッシュにも、どちらにも余裕が出ます。ただし広がるのは GPU の中だけです。
アプローチ 2:prefillキャッシュを GPU の外まで延ばす(上限 2 に効く)。 GPU から追い出された prefillキャッシュを捨てずに CPU 側の RAM へ退避し、同じ前置きが来たら戻します。
この 2 つ目は、効きました。 冒頭に書いたとおり、外に出しても足すのは 24 ms だけ。溢れた瞬間に「2 問目以降が速い」が崩れる、という運用の崖が消えます。
ただし最後まで通ったのは、対照に置いた dense の Qwen3-4B でした。主役のハイブリッド Qwen3.5-4B は、退避までは動くのに読み戻す経路が今日の vLLM ではまだ開いていません。分けているのはモデルの新しさでも賢さでもなくアテンションの構造で、しかもその構造は、GPU の中に置ける量ではむしろ Qwen3.5 を有利にしています(同じカードで 1.7 倍。後述)。どこが開いていてどこが閉じているのか、ソースまで辿って以下で特定します。
計測は 2026 年 6 月 29 日・vLLM 0.21.1rc1.dev243 で行いました。以下で出てくるソースの行やガードの位置は、このビルドの断面です。各条件 1〜数試行の単発計測で、狙いは「どこまで動き、どこで止まるか」の境界を示すことです。
要点
- GPU から溢れても「速い側」に留まれます:押し出された前置きを CPU の RAM に預けておけば、次の同じ前置きは 254 ms で返ります(全部計算し直すと 602 ms、GPU に残っていれば 230 ms)。外に出しても足すのは 24 ms だけ。運ぶのは prefill の 1 回きりなので、生成速度は落ちません。しかも増設するのは VRAM ではなく、桁違いに安い RAM です。
-
GPU の中を厚くする手も 2 つ:
--kv-cache-dtype fp8の 1 行で KV プールが 70,284 → 127,622 トークン(1.82 倍)、50 問の採点は 1.900 → 1.880 でほぼ不変。さらにハイブリッドは K/V を積み上げないぶん、同じ 12GB に dense の 1.7 倍(40,432 → 67,435 トークン)が最初から載ります。 - ただし、いまの Qwen3.5 は CPU から戻せません:GPU 内のprefillキャッシュ(cold 約 2,646ms → 約 83ms)も CPU への退避も動くのに、戻す経路だけが vLLM 側で未実装でした。「ガードを外せばいい」を 2 通り実際に試して、両方とも否定しています。塞いでいるのはモデルの性質ではなく実装状況なので、バージョンが変われば変わります。
- 分けているのはモデルの優劣ではなく、経路の有無:フルアテンションの KV はもともとブロック単位で切り出せるので CPU から戻せる。線形アテンションの畳んだ状態は、ブロック境界で切る正当性がまだ検証されていない——ここまでソースと dense 対照実験の両方で絞り込みました。構造の限界ではなく、実装が追いついていないだけです。
ここから本文
背景:なぜ置き場所を広げたいのか
長い前置きを毎回固定する運用は、1 つの有限資源を 2 つの用途で奪い合う運用でもあります。ローカルは入力トークンに課金がないぶん前置きを伸ばす動機が強く、そのぶん奪い合いも激しくなります。
本構成(RTX 4070 12GB・--gpu-memory-utilization 0.90・--max-model-len 32768)で確保された KV プールは 70,284 トークンぶん、1 リクエストに載せられるのは 32,768 トークンまででした。この 2 つは独立ではありません。走っているリクエストが握るぶんを引いた残りが、そのまま取り置きに使える枠です。上限いっぱいのリクエストが 1 本走れば残りは 37,516 トークン、2 本なら残りは 4,748 トークン——取っておける枠はほぼゼロです。
CAG の「2 問目以降が速い」は、この取り置きが evict されずに生き残っているあいだだけ成り立つ性質です。そして生き残れるかどうかを決めるのは、資料の長さ × 本数と、そこへ割り込む他のリクエストです。資料 1 本を独りで叩いているうちは、何も押し出されません。 載せる資料が増え、使う人が増えた時点で、取り置きは奪い合いになります。
この 2 つの上限に、1 つずつ手を入れます。上限 1 は 1 行で片がつきました。この記事の本題は上限 2 のほう——GPU から溢れたぶんを、CPU の RAM まで延ばせるかです。
検証環境
- ハードウェア: GPU = RTX 4070 12GB / CPU + RAM(CPU オフロード先として 8GiB を割当)
- OS: Ubuntu 24.04
- 推論基盤: vLLM 0.21.1rc1.dev243+ga5bbd81e2(Docker イメージ vllm/vllm-openai:nightly)/ V1 engine
- 主要モデル:
- RedHatAI/Qwen3.5-4B-FP8-dynamic … Gated DeltaNet ハイブリッド(線形24層 : フル8層)
- RedHatAI/Qwen3-4B-FP8-dynamic … dense 純フルアテンション(36層)※対照実験用
- 量子化: 重み FP8、KV キャッシュは実験により BF16 / FP8 を切替
- 計測: 時間は HTTP 実測(max_tokens=16 の非ストリーム総時間を TTFT の近似として使う)。キャッシュのヒットとトークンの出所は Prometheus /metrics を正準とする
- 試行: 各条件 1〜数試行・ウォームアップ後の値。temperature=0.0
時間だけを見ていると「速くなった理由」が推測になります。そこで、サーバが出している統計からプロンプトのトークンを出所ごとに数えました——再計算したぶん/GPU 上のprefillキャッシュから来たぶん/CPU から運んできたぶんの 3 つです。このビルドは API の応答からキャッシュの効きが判別できなかったので、以下はすべてこの統計を正準にしています。
まず 1 行で広げる、それから GPU の外へ
上限 1 のほうは、あっけないほど簡単に片がつきました。K/V を 8bit で持つよう 1 行足して起動し直すだけです。それだけで KV プールは 70,284 → 127,622 トークン(1.82 倍)。50 問の採点は 1.900 → 1.880 で、ほぼ変わりません。1 リクエストに載せられる長さにも、次に備えて取っておける枠にも、同じ 1 行で一緒に余裕が出ます。ここまでは気持ちよく進みました。
ただし、広がったのは GPU の中だけです。プールが 1.82 倍になっても、載せる資料が増えれば取り置きはやはり押し出されます。押し出されたぶんは捨てるしかないのか——ここからが本題です。
prefillキャッシュを CPU の RAM に逃がす
prefillキャッシュとして残しておきたいぶんが古い順に追い出されるとき、捨てずに CPU 側の RAM へ退避し、次に同じ前置きが来たら GPU へ戻す。これが CPU オフロードです。
なぜ RAM に置くと得なのかは、ヒット時にやることを並べれば分かります。前から順に「同じ前置きか」を照合し、一致したぶんを CPU から GPU へ運んで、続きだけを計算する。計算そのものはやり直しません。 しかも運ぶのは prefill の 1 回きりで、そのあとの生成は GPU 上のぶんしか見ないので、1 トークンあたりの生成速度は変わりません。伸びるのは最初の 1 トークンが出るまでの時間だけです。
つまり払うのは、メモリを跨いで運ぶぶんのわずかな遅延だけ。対価は前置きの再計算がまるごと消えることです。前置きが長いほど割に合います。 RAM は VRAM より桁違いに安く積めるので、GPU に置ききれない前置きを何本も抱えられます。
この方式で GPU の外に出せるのは、誰も参照していないprefillキャッシュのブロックだけです。実行中のリクエストが握る KV 列ごと CPU へ swap out する方式もあり得て、vLLM も V0 系では持っていましたが、V1 はこれをやめて再計算に一本化しているため、今回の構成にその経路はありません。
いずれにせよ、上限 1(資料の長さ)は 1 トークンも伸びません。デコードは 1 トークン進むごとに過去の全 K/V を読むので、参照されている KV は GPU に載っている必要があるからです。効くのは 2 回目以降の TTFT だけです。
CPU 側に 8GiB の退避先を持たせて起動し、同じ 2,497 トークンの前置きで cold → GPU ヒット → 別の資料を 40 本挟んで押し出す → 再要求、の順に測ります。3 段目は意図的に作った状況です。資料 1 本を独りで叩いているだけなら prefillキャッシュは GPU に残り続け、そもそも CPU オフロードの出番がありません。
挟んだ資料そのものは、ハイブリッドで各 約 454 ms で返っています。ほぼ同じ手順を、対照用の dense(Qwen3-4B)でも走らせました(押し出しに挟む本数だけ、あとで触れる理由で変えています。また同じ資料でも、モデルが違えばトークン数は変わります——ハイブリッド 2,497 に対し dense は約 2,900 トークンぶんでした)。手法そのものが成立するのかどうかが先に見えるので、並べて出します。
| 同じ資料を使ったときの TTFT | ハイブリッド Qwen3.5-4B | dense Qwen3-4B |
|---|---|---|
| cold(全再計算) | 約 2,646 ms | 約 602 ms |
| GPU 上のprefillキャッシュにヒット | 約 83 ms(約 32 倍速) | 約 230 ms |
| 押し出されたあと、CPU から読み戻し | engine が即死(HTTP 500) | 約 254 ms(2,880 トークン転送) |
dense は最後まで通りました。 転送カウンタが 2,880 トークン立っているので、KV が実際に CPU の RAM から GPU へ運ばれたことまで確認できます。収支もそのまま出ています——GPU ヒット(230 ms)に 24 ms 足すだけで、再計算の 602 ms を避けられている。上で書いた「照合して運ぶだけ」が、そのまま数字になった形です。
うれしいのは 24 ms という数字そのものより、GPU に置ききれなくなっても「速い側」に留まれることです。取り置きが押し出された瞬間、本来なら 602 ms の全再計算に逆戻りします。RAM さえ積んでおけば 254 ms のまま——資料が増えても使う人が増えても、CAG の「2 問目以降が速い」が崩れません。 CPU オフロードという手法そのものは、ちゃんと効きます。
ハイブリッドはそもそも、GPU に多く持てる
ここで 2 つのモデルの持ち方の差を挟みます。同じ 12GB でも、GPU に入る量が違うからです。
dense は 36 層すべてがフルアテンションで、層ごと・トークンごとに K/V を積みます。ハイブリッドで積むのは 8 層ぶんだけで、残りの層は畳み込んだ状態を 1 個持つだけです。実測でも、退避先を確保した同じ設定で dense は 40,432 トークンぶん、ハイブリッドは 67,435 トークンぶん——同じカードで 1.7 倍入ります(冒頭の 70,284 トークンと値が違うのは、CPU 側の退避先を持たせた状態での実測だからです)。
つまり Qwen3.5 のほうが、GPU の中だけで戦える余地が広い。長い資料を載せる余地も、次に備えて取っておける枠も最初から広く、そもそも GPU の外へ出す必要が起きにくいモデルです。
(逆に初回だけはハイブリッドのほうが遅く、より短い前置きで 2,646 ms 対 602 ms でした。原因までは切り分けていません。効いてくるのは 2 回目以降なので、前置きを固定して使い回す用途では、GPU に残っているうちの 83 ms のほうが体感に出ます。)
(押し出しに挟む資料の本数も、この差に合わせてハイブリッド 40 本・dense 16 本と変えています。dense は GPU 側が先に埋まるうえ、40 本も挟むと 8GiB の CPU 側まで溢れて、肝心の資料自身が CPU からも消えてしまうためです。退避先の RAM にも上限がある、という当たり前の事実がここで効きます。)
でも、いまの 3.5 には残る問題がある
とはいえ 1.7 倍は 1.7 倍で、資料と利用者が増えれば結局は溢れます。そこから先を RAM に預けられるかどうかが、この記事の分かれ目です。そしてハイブリッドは、押し出しまでは動くのに、戻そうとした瞬間だけが通りませんでした(退避の登録は済んでいて、サーバも「その前置きは CPU 側に持っている」と申告できています)。なぜそこだけが閉じているのか、ここから特定します。
なお、退避のしくみは実装が何種類かあり、いちばん名の通った LMCache は起動すらしませんでした。ハイブリッドは「フル層のブロック KV」と「線形層の畳んだ状態」という 2 種類を抱えるのですが、それをひとまとめに扱う仕組みに LMCache 側が対応しておらず、起動時に「1 つの型に統一できない」と言って止まります。モデルではなく実装側の話なので、ここで「ハイブリッドではオフロードは無理」と結論づけると間違えます。
止めていたのは 2 行、いま残っているのは 1 行
vLLM のソースをたどると、止めているのは 2 か所でした。どちらもバグではなく、「まだ検証していない/まだ実装していない」と書き置かれた意図的な停止装置です。
1 つはサーバ側。外部から KV を持ち込む経路に「まだ検証していない」と書かれた 1 行があり、CPU から戻した瞬間にそこで止まります。ただしこの 1 行は、計測の 3 週間前に upstream から削除されていました(PR #42554、2026 年 6 月 4 日マージ・v0.23.0 以降)。踏んだのは、それより古い nightly のほうです。
もう 1 つはモデル側で、こちらは今日の最新コードにも残っています。 Qwen3.5 のクラスは「前置きの使い回しに対応済み」の印を持たず、印のないモデルをサーバが互換モードへ落とします(起動ログに出る --mamba-cache-mode=align がこれです。all を指定しても無視され、align に戻されます)。復元の経路が用意されているのは、その互換モードの外側だけです。「対応済み」と申告できているのは、初期からある線形アテンション系(Mamba / Mamba2 / Jamba など)のモデルだけでした。
つまりモデル側は二段構えです。印がないので互換モードへ落とされ、その互換モードには復元の経路がない。かといって印を立てれば、今度は「そのモードは未実装だ」とモデル自身が例外を投げる——このあと、実際に両方やってみます。
実物の 2 行(vLLM のソース・リンクつき)
① サーバ側の停止装置 — 計測に使ったビルド(コミット a5bbd81e2)の scheduler.py#L286-L288
assert num_external_computed_tokens == 0, (
"External KV connector is not verified yet"
)
② モデル側の停止装置 — 今日時点の main、qwen3_5.py#L332-L336(このあと実際に踏みます)
if cache_config.mamba_cache_mode == "all":
raise NotImplementedError(
"Qwen3.5 currently does not support 'all' prefix caching, "
"please use '--mamba-cache-mode=align' instead"
)
① は今日の main では見つかりません。② は残っていて、Qwen3.5 のクラスは今も「対応済み」の印を継承していません。main 側の行番号は更新でずれます。
「止めてあるだけなら外せばいいのでは」と思ったので、両方やりました。
- サーバ側の 1 行を削除 → クラッシュは消えますが、今度は engine が固まります。CPU 側への問い合わせは増えるのに、運ばれたトークンは 0。
- モデル側に「対応済み」の印を立てる → 強制フォールバックは避けられますが、今度はモデル自身が「未対応」と言って初期化中に落ちます。
ここから分かることがもう 1 つあります。サーバ側の 1 行は今日の版では削除済みなので、最新の vLLM で同じことをすると、HTTP 500 で落ちる代わりに応答が返らないまま固まります。 下の表の「即死」は、古い nightly を踏んだときの症状です。
つまりこの 2 行は無意味なガードではなく、その先が実装途中であることを示していただけでした。
分かれ目は「線形アテンション層を持つか」だけ
なぜ dense だけ通ったのか。この一連のガードは、線形アテンション層を持つモデルのときにしか作動しません。 純フルアテンションなら、問題の経路をそもそも一度も通らないからです。だから対照には、同じ配布元・同じ 4B・重みも同じ FP8 のまま、アテンションだけ純フルにした Qwen3-4B-FP8-dynamic を選びました。起動の時点で差が出ます——dense は警告なしで立ち上がり、ハイブリッドは「未対応」扱いで互換モードへ落とされます。
厳密には、この 2 モデルは層数(36 対 32)やマルチモーダル対応の有無も違うので、「アテンション構造だけを変えた」完全な統制実験ではありません。ただし同じ手順で結果が真っ二つに分かれ、しかもソース上の分岐も「線形アテンション層を持つか」という 1 つの判定から始まっている——この 2 つが揃うなら、効いているのは線形アテンション層の有無だと読むのが自然です。
答え — 切り出せる KV と、切り出せない再帰状態
フルアテンションの KV は「トークン 0〜255 ぶんの K/V」という形で取り出せます。だからブロック単位で GPU から剥がし、CPU に置き、また戻せる。prefillキャッシュという仕組みそのものが、この分解可能性の上に成り立っています。
線形アテンションの再帰状態は違います。全トークンを畳み込んだ 1 個の状態で、「先頭 256 トークンぶんの状態」にあたる部分が定義できません。長さに比例しないから安く常駐でき、長さに分解できないから外へ持ち出せない。ブロック境界で切って退避し、また戻す操作の正当性が、このビルドではまだ検証されていない——だから vLLM は、部品は先に用意しつつ、モデル側に「未対応」と明示して、戻す経路だけを開けずにいます。
同じ「KV キャッシュ」という言葉で呼ばれていても、中身の持ち方が違えば、外に出せるかどうかも変わる。これが今回の答えです。
それでも、ハイブリッドの持ち方は効率がいい
外に出せないことは、GPU の中で不利だということではありません。さきほどの 1.7 倍(40,432 対 67,435)は、溢れるまでの距離そのものを長くしているということです。安く持てる理由と、外に出せない理由が同じ——弱点というより、設計上の選択です。
だから今日の選び方はこうなります。GPU に載る範囲で足りるなら、効率よく持てるハイブリッドが有利。載る範囲を超えて増え続けるなら、外に出せる dense が安全。 そして「外に出せない」ほうは構造の限界ではありません。ハイブリッド構成を扱える退避の実装も、線形アテンション層の状態を運ぶコードも、すでに置かれています。閉じているのは入口とその先だけで、開けにいく動きも出ています(後述)。
補足:この結果は、この環境だけの話ではない
GPU 1 枚で出た結果は、まず自分の環境固有の事故を疑うべきです。hybrid モデルの KV オフロードを扱う issue #38230 には、**2026 年 8 月 20 日付で「nightly で今日もまだ起きている。Qwen 3.5〜3.8 は KV オフロードと併用できない」**という別の人のコメントが付いていました。
上で見たモデル側の 1 行が今日の main にも残っていることと、この報告は符合します。
一方で、issue を立てた本人は自前で改造したフォークなら Qwen3.5-4B-FP8 でも動いたと報告していて、対応する PR #38261 も出ています(本稿執筆時点で未マージ)。共通部品もその後前進しています。開けにいく動きはある、というのが現在地です。
まとめ
API で LLM を呼んでいるあいだ、KV プールは一切見えません。どれだけの大きさで、何を残して何を捨てるかは、すべて提供側が決めています。自分で GPU に載せると、それが自分の仕事になります。
- 溢れても「速い側」に留まれます。 GPU から押し出された前置きを RAM に預けておけば、次の要求は 254 ms で返ります。全部計算し直せば 602 ms、GPU に残っていれば 230 ms——外に出しても、GPU ヒットに 24 ms 足すだけです。運ぶのは prefill の 1 回きりで、生成が始まればコストはかかりません。溢れた瞬間に「2 問目以降が速い」が崩れる、という運用の崖をなくせます。
-
しかも、そのために増設するのは RAM です。 VRAM を積み増すのに比べれば桁違いに安く、GPU に置ききれない前置きを何本も抱えられます。プール自体を広げるほうも
--kv-cache-dtype fp8の 1 行で 1.82 倍・精度ほぼ不変ですが、増えるのは GPU の中だけ。外まで伸ばすのは RAM への退避のほうです。 - どこまで外に出せるかは、モデルの構造で決まります。 見るのはパラメータ数でも世代でもなく、線形アテンション層を持つかどうかの 1 点。持たないなら CPU まで延ばせる(254 ms で復元)、持つなら GPU に載っているあいだだけ。同じ 4B・同じ FP8 でも、ここで運用の設計が変わります。
- 未実装であって、不可能ではありません。 部品は揃っていて入口だけが閉じている状態です。塞がっているのはモデルの性質ではなく vLLM の実装状況なので、判断にはバージョンを添えてください。
サービング側に立つと、「モデルを差し替える」は重みを差し替えることではなくなります。キャッシュをどう持つか、どこまで動かせるか、という内部の経路ごと差し替わる。 線形アテンションは、同じ 12GB に 1.7 倍を載せるという形で先に進んでいて、あとは載りきらないぶんを外へ出す経路が追いつくのを待っている段階です。片方はもう CPU まで届き、もう片方はまだ GPU の中——どちらも「このバージョンでは」の話です。API の向こう側では、この確認が毎回行われています。
計測に使った起動設定・スクリプト・パッチは参考のリポジトリにあります。いずれも vLLM 0.21.1rc1.dev243+ga5bbd81e2 での観察で、別バージョンでは対象行が変わります。
詰まった点・誤り・「うちのカードではこうだった」があれば、コメントで教えてください。追記します。役に立ったら LGTM を、読み返す方はストックが便利です(更新時に通知されます)。
参考
- ローカルLLM を動かすなら RAG より速くて正確な選択肢がある——CAG の実装と設計(実装編)(本シリーズ)
- 〈結果編〉ローカルLLMで CAG は RAG に勝てるか — 速度・精度を50問で実測(本シリーズ)
- 〈RAG 解剖編〉なぜRAGは"しれっと嘘"をつくのか
- VRAM 12GB 環境で FP8 KV Cache は実用投入できるか — テキスト性能評価(本記事の 1.82 倍の前提。品質面の単体検証)
- KV Cache 共有境界の実測: 問題編 / 対策編
- vLLM — Automatic Prefix Caching
- vLLM issue #38230(hybrid モデルの KV オフロード。2026-08-20 時点でも未解決の報告あり/フォークでの動作報告あり)
- vLLM PR #42554(Mamba prefix caching mode 対応。本記事が解剖した scheduler の assert はこの PR で削除され、v0.23.0 以降には存在しない)
- vLLM PR #51100(Mamba all-mode CPU オフロードの境界整合。2026-08 マージ済み)
- vLLM PR #38261(issue #38230 に対応するフォークの PR。本稿執筆時点で未マージ)
- vLLM PR #41445(offloading connector の HMA 対応。2026-05 マージ済み)
- vLLM RFC #36780(hybrid SSM-FA モデルの connector 対応。活動停止により自動クローズ)
- 実装コード・計測スクリプト一式(本シリーズのリポジトリ): ai-systems-notes/local-llm-rag-cag-benchmark