KV キャッシュをネットワーク上に置くべき理由

巨大な KV キャッシュをネットワーク越しに運ぶのは無理に思えます。DRAM、ローカル NVMe、リモート Valkey を測定し、ネットワーク経由の実際のコストを調べたところ、予想外の結果になりました。

Allen Helton Allen Helton
KV キャッシュ向けの DRAM、ローカル NVMe、リモート Valkey を競走路で比較する図

以前、デスクで少し手を止めていると、同僚に「何をしているの?」と聞かれ、「コンパイルが終わるのを待っているだけ」と答えていたものです。2026 年、その言い訳は「Claude の返答を待っている」に変わりました。

冗談めかして言っていますが、事実でもあります。推論は、速さで知られているわけではありません。強力ではあります。しかし、速いとは言えません。

高速化のために、私たちはさまざまな実験をしてきました。並列取得、ゼロコピー読み取り、メタデータ参照のバッチ化など、取得経路全体が対象です。その中に、大きな利点と引き換えに、大きなトレードオフもありそうな、特に興味深い実験がありました。KV キャッシュをローカルではなくリモートに保存したらどうなるでしょうか。ただし、一つ問題があります。KV キャッシュは巨大です。それをネットワーク越しに運ぶのは、最初から無理な話ではないでしょうか。

一般的な順序はわかっています。GPU メモリが最速で、次にホストの DRAM、同じマシンに搭載された NVMe、そして、どうしても必要ならネットワークの向こう側です。共有が必要なときにリモートを使い、その代償としてレイテンシを支払います。

ただし、これは私だけの新しい発想ではありません。業界全体が今、取り組んでいます。NVIDIA は Dynamo にオフロード階層を用意しています。Mooncake はストアを構築しています。Xinnor は 6 月、クライアント側キャッシュを備えた Lustre ならローカル NVMe に対抗できると論じる記事を公開しました。Khawaja Shams は、プリフィルとデコードを分離すれば、望むかどうかにかかわらず、キャッシュをマシン間で移動させる必要があると述べています。

利点があることは明らかです。しかし、そのトレードオフのコストについては、あまり情報が見当たりませんでした。Valkey のようなシステムに KV キャッシュを保存すると、実際にどれほどレイテンシが増えるのでしょうか。そのコストを払っても、プリフィルを省略する価値はあるのでしょうか。

そこで、自分で測定することにしました。

検証環境

ベンチマークでは 3 台のマシンを使います。vLLM を実行する同一構成の GPU ノード 2 台と、その両方から 10 Gbps のネットワークで 1 ホップ先にある Valkey マシンです。

ベンチマーク構成:gpu-a が KV を計算し、マシン内に DRAM と NVMe の階層を持ちます。Valkey は 10 Gbps で 1 ホップ先に共有キャッシュを保持し、同一構成の gpu-b は自身の各階層が空の状態から開始します。

gpu-a がプリフィルの計算をすべて行います。DRAM 階層と NVMe 階層はその内部にあり、Valkey はネットワークの向こう側です。gpu-b は、プリフィルを行っていないマシンに、キャッシュ済みのリクエストが届くとどうなるかを調べるために使います。

ワークロードは、現実に即して生成した法律・医療文書 30 件で、すべて正確に 10,000 トークンにそろえました。各文書を vLLM に 2 回送ります。1 回目はプリフィルを実行して KV キャッシュを保存します。2 回目は同じプロンプトを送り、最初のトークンが返るまでの時間(TTFT)を測定します。実行ごとに変えるのは、KV キャッシュの保存場所だけです。

すべての環境で vLLM 0.28.0、LMCache 0.5.4、valkey-glide-sync 2.5.1、Valkey 9.1.1 を使い、NVIDIA L4 上で Qwen2.5-7B-Instruct-AWQ を提供します。

正確な結果を得るため、vLLM のプレフィックスキャッシュを無効にしました。これにより、繰り返しリクエストがすべて GPU 上で処理されるのを防ぎます。また、毎回の実行前にディスク階層を消去し、ページキャッシュも破棄する必要がありました。Linux は最近ディスクに書き込んだデータをメモリに保持するためです。そうしなければ、「NVMe」の読み取りが RAM から行われ、実際のディスク速度を検証したことになりません。

目標は、3.1~3.3 秒のコールド TTFT を下回ることでした。別のインスタンスで、1 日間隔を空けてベンチマーク全体を 2 回実行したところ、結果は 2% 以内で再現できました。そのため、この結果には手応えを感じています。ネットワークがどうだったかを見ていきましょう。

すべてを 1 台で処理する

最初のテストでは、gpu-a がすべての作業を担います。KV を計算し、検証対象の階層に保存して、2 回目に読み戻します。Valkey のテストでも、自分のキャッシュを書いて読むのは gpu-a です。ローカルバスの代わりにネットワークを通る点だけが異なります。

階層キャッシュ利用時の TTFT高速化倍率再利用
DRAM (8 GB)3345 ms0.95x0/30
ローカル NVMe1521 ms2.18x30/30
Valkey(リモート)557 ms5.98x30/30

見間違いではありません。ネットワークが最速でした。しかも、大きな差をつけています。同じマシンのローカル NVMe に対して 2.7 倍高速でした。ディスクは筐体内にあり、間にスイッチもないのに、ネットワーク越しの方がキャッシュへ速くアクセスできたのです。

(DRAM の行はいったん置いておきます。興味深いことが起きたので、後で説明します。)

最初は、何かを間違えたのだと思いました。「ディスク」階層をインスタンスストアではなく EBS のルートボリュームに置いてしまったのでしょうか。しかし、lsblk では階層が nvme0n1(250 GB のインスタンスストア)上にあることを確認でき、df では実行中に 16 GiB が書き込まれていました。300,000 トークン分の KV として、おおむね妥当な量です。

計算してみると、理由が見えてきました。

各文書の KV キャッシュは、256 トークンのチャンク 39 個で構成されます。1 トークンあたり 57,344 バイトなので、約 572 MB(39 x 256 x 57,344)です。検証用ハーネスの動作を確認する際に、すべてがメモリに収まる文書 2 件で DRAM 階層を試したところ、ヒットは約 82 ms で返りました。DRAM ヒットではほとんど転送が発生しないため、82 ms は基本的にオーバーヘッドと考えられます。それを超える時間は、転送時間と見なせます。

つまり、Valkey の 557 ms のうち、転送は実質約 473 ms です。572 MB を 473 ms で運ぶと 1.2 GB/s になり、10 Gbps の NIC を使い切っていることになります。一方、ディスクは 572 MB の転送に約 1,438 ms かかりました。400 MB/s です。NVMe なら 2 GB/s を超える読み取りができるはずなので、不思議な値です。

したがって、リモート階層はネットワークがボトルネックで、ローカル階層は自分自身のソフトウェアがボトルネックです。LMCache の Valkey コネクターには、今年かなりの改善が加えられたことがわかります。8 個のワーカーが並列に、呼び出し元が所有するバッファーへ直接データを取得します。ローカルディスクの経路には、まだ同じだけの改善が行われていません。NVMe で 400 MB/s という値は、大きな改善余地を示しています。この状態が長く続くとは思いません。

ともあれ、この結果では、KV キャッシュをネットワーク上に置いても、予想したレイテンシの不利はありませんでした。物理的な限界に近い速度で動作していました。仮に来月、ディスクの経路が改善されて 1.5 GB/s に達しても、ネットワークは十分に有力な選択肢です。さらに、Valkey はより高速な NIC を使えば、もっと速くなる余地があります。

ただし、一つ説明できないことがあります。ディスクの 2.18 倍という高速化は中央値ですが、文書別の値には奇妙な傾向がありました。キャッシュ利用時に最初に要求した文書は、約 11 倍速く返ります。2 件目は約 3.5 倍。その後の 28 件は、どれもほぼ 2.1 倍です。不思議なことに、2 回の実行でほぼ同じ結果になりました。

キャッシュ利用時の処理順1 回目2 回目
1 件目11.42x10.84x
2 件目3.65x3.28x
残りの 28 件~2.1x~2.1x

最初の 1~2 リクエストでは何かがウォームな状態で、その後はそうではなくなるようです。原因はわかりませんし、もっともらしい理由を作るつもりもありません。何が起きているにせよ、これは引用している中央値よりディスクをよく見せる現象なので、結論を左右するものではありません。

DRAM について

先ほどの結果表では、DRAM の「高速化倍率」は 0.95x でした。設定を間違えたのでは、と思うかもしれません。しかし、間違いではありません。文書 2 件で確認した際に、ヒットが 82 ms で返ったことを覚えているでしょうか。同じ階層、同じ設定での結果です。TTFT はコールド時が 3122.8 ms、キャッシュ利用時が 82.5 ms で、37.93 倍の高速化でした。

DRAM は遅くありません。スタック全体で、圧倒的に最速です。ただ、容量が足りなくなるのです。

文書 30 件は 300,000 トークン分の KV です。1 トークンあたり 57,344 バイトなので、16 GiB になります。この階層に割り当てたのは 8 GB で、ホスト全体にも 16 GB しかありません。単純にメモリが足りず、2 回目を実行する前にすべて追い出されていました。

それだけではありません。キャッシュから何も提供できなかったすべての階層では、30 件中少なくとも 29 件で、コールド時よりも 3~5% 遅くなりました。ミスする参照にも参照処理は必要であり、追い出しが起きるストアでも書き込みのコストはかかります。このケースでは、小さすぎるキャッシュが負担になっていました。

しかも、文書 30 件など、本番環境のコーパスと比べればごくわずかです。ワーキングセットが文書 2 件なら、DRAM を使えば十分です。30 件以上なら、DRAM より容量の大きいものを選んでください。

2 台に拡張する

ここまでの検証は、1 台のマシンが自分のキャッシュを書いて読み戻すものでした。しかし、本番環境ではリクエストが別のマシンへ送られます。そのときどうなるかが、大きな問いです。そこで、コールド時の処理を gpu-a で、キャッシュ利用時の処理を、プリフィルを行っていない gpu-b で実行しました。

階層1 台のノード2 台目のノード2 台目での再利用
DRAM0.95x0.96x0/30
ローカル NVMe2.18x0.95x0/30
Valkey5.98x5.97x30/30

予想どおり、gpu-b には KV がなく、アクセスできるデータもないため、NVMe の再利用はゼロになり、プリフィルに 3.2 秒かかりました。分散環境で使い始めると、NVMe の魅力はかなり薄れます。

一方、Valkey は 5.98 倍から 5.97 倍になっただけでした。KV を計算していないマシンから読み取っても、測定可能な追加コストはありません。最も悪い文書では 5.55 倍から 3.92 倍に下がりましたが、中央値は実質同じです。このキャッシュは、どちらか一方のノードに属しているわけではありません。それこそが目的でした。

検証から得られたこと

ワーキングセットがホストの DRAM に収まるなら、それを使ってください。キャッシュヒットは、プリフィルを再計算するより 37 倍速く返りました。ただし、容量を正確に見積もる必要があります。コーパスをトークン数で測り、モデルのトークンあたりの KV サイズを掛けてください。目分量で判断してはいけません。

収まらなくなると、ローカルディスクは書き込んだマシン上で 2.18 倍の効果を出しますが、他のマシンでは効果がありません。単一ノードでは大きな改善です。しかし、フリート全体では、各エントリが生成元のマシンに閉じ込められ、ヒット率は同じマシンに再度ルーティングされる確率に依存します。

ネットワーク階層への書き込みにもコストはあります。Valkey を使うコールド TTFT は DRAM より約 110~150 ms 遅く、メモリではなくネットワーク越しに保存するコストは 3~4% でした。つまり、キャッシュを埋めるたびに 110~150 ms を払い、その後のヒットごとに約 2.7 秒を取り戻すというトレードオフです。

最も意外だったのは、この環境では共有階層の方がすでに速かったことです。このハードウェア、このモデル、このコーパスサイズでは、キャッシュを別のマシンに置く方が速くなりました。検証前は、レイテンシのコストを払っても共有には価値がある、と論じることになると思っていました。この予想が外れたのは、うれしい結果です。プリフィルの省略には、かかるコスト以上の価値があります。

KV キャッシュはネットワーク上に置くべきです。共有が必要だからという理由だけではありません。今回、そこで最も速く動いたからです。

自分で試す

検証用ハーネス、30 件の文書コーパス、各階層の設定、インフラ用スクリプトは、すべて valkey-kvcache-bench にあります。一つのコマンドで 3 台すべてをプロビジョニングし、全構成を実行し、結果を取得して、すべてを終了します。

cd infra
.\Run-TierBench.ps1 -AwsProfile <profile> -Teardown

コストは 1 時間あたり約 $2.14 で、ベンチマーク全体は 1 時間未満で終わります。

自分で試す場合に、一つ注意点があります。最初に 2 台構成のテストを実行したときは、Valkey を含め、どの階層でも 0/30 になりました。LMCache のデフォルトのキャッシュキーハッシュは Python の hash() であり、プロセスごとに異なるランダムなソルトが使われていたためです。つまり、gpu-a と gpu-b は同じトークンから異なるキーを生成し、ノード間の再利用ができませんでした。LMCache は起動時に Using builtin hash without PYTHONHASHSEED set という警告まで出していたのですが、私は見落としていました。次の設定値で修正できます。

pre_caching_hash_algorithm: sha256_cbor_64bit

共有 KV キャッシュのノード間ヒット率がゼロになったら、おそらくこれが原因です。

ぜひ試してみてください。異なる結果が得られた場合は、比較できるよう、お気軽にご連絡ください。

それでは、よい開発を!

あわせて読みたい