第3話. TANREN — 進化したキャッシュ方策を、本物の vLLM に差し込んで測る

左が vLLM の既定の追い出し、右が TANREN の進化方策です。実際の会話データ(ShareGPT)を本物の vLLM に流して測った TTFT の再生で、注目してほしいのは「テール応答」——遅い側の応答時間です。

本記事は TANREN 連載の第3話です。プロジェクト全体像は こちら、第1話(Atari Pong)は こちら、第2話(キャッシュ追い出し)は こちら

TANREN を 30 秒で

TANREN は、LLM の「重み」を莫大なコストをかけて学習させる代わりに、追加学習なしの安価な LLM に候補コードを書かせ、そのプログラムを自動採点しながら「進化」させていく仕組みです。大規模計算が数億フレームの学習の末に到達した水準に、手元の PC 1台・数日間の探索・数ドルの API 代で迫ることを目指しています。

第2話では、進化したキャッシュ方策が Twitter 本番トレースの再生——つまりシミュレーション——で、20 年の定番アルゴリズムに 11 勝 1 敗しました。当然、次の疑問が出てきます。シミュレーションでの勝ちは、本物のサーバでも勝ちなのか? 今回はシミュレータを出て、稼働中の本物の vLLM にコードを組み込み、実機で測ります。先に書いておくと、一度は負けました。その原因を突き止める過程も含めて、そのまま記録します。

何をやったのか

LLM サーバは、会話の共通の冒頭部分の計算結果(KV キャッシュ)を保存して再計算を省きます。prefix caching と呼ばれる仕組みで、vLLM にも組み込まれています。ヒットしたときの効果は大きく、参考の別測定では 2,048 語のプロンプトで TTFT が 1,500ms → 177ms(−88%)でした。ただしメモリは有限なので、足りなくなったときに「どのブロックを捨てるか」を決める必要があります。第2話と同じ「捨て方」の問題が、今度は本物のエンジンの中にあるわけです。

やったことは 3 つです。

  1. モデルの重みには一切手を付けず、数十行の読める追い出し方策を進化で作った。
  2. それを vLLM の該当ファイル 1 つに組み込んだ(1 ファイルのパッチで、1 行の変更で元に戻せます)。
  3. 既定の追い出しと進化方策のそれぞれに、完全に同一の実会話列(ShareGPT のうち、進化に使っていない区間)を再生して A/B 比較した。

実機は NVIDIA GB10、vLLM 0.19.1、モデルは Llama-3.2-1B です。キャッシュへの負荷は --num-gpu-blocks-override で固定し、決定論的に再現できるようにしました。

結果 — 実測のまま書きます

メモリがちょうど足りなくなる負荷(キャッシュの奪い合いが起きる領域)で、進化方策は既定に対して次の結果でした。

指標効果
テール応答(p99 TTFT)−13〜16% 短縮
キャッシュヒット率+約1ポイント

TTFT は最初の 1 文字が返るまでの時間、p99 は「遅い側の 1%」の値で、実運用の SLO に書かれる数字です。つまり「混雑時に、いちばん待たされる応答が 13〜16% 速く返り始める」——これが実機で出た効果です。ブロック数×シードごとの実測は次の通りです。

キャッシュ容量(blocks)/ シードp99 TTFTヒット率
2000 / s174ms → 69ms(−7.9%)42.6% → 44.4%(+1.8pt)
2000 / s271ms → 68ms(−3.9%)44.4% → 44.7%(+0.4pt)
2500 / s172ms → 62ms(−13.4%48.4% → 50.4%(+2.0pt)
2500 / s272ms → 61ms(−14.2%50.3% → 50.7%(+0.4pt)
3000 / s172ms → 61ms(−15.4%53.1% → 54.4%(+1.3pt)
3000 / s271ms → 60ms(−15.4%54.0% → 55.1%(+1.1pt)

メモリがちょうど枯渇し始める境界(blocks 2500〜3000。以下「膝」と呼びます)で効果が最大になり、その手前(2000)では小さくなる——効果は負荷に依存するという構造も、そのまま数字に表れています。

先に、正直な代償も書いておきます。

  • 効果が出るのはメモリに負荷がかかっている時だけです。サーバが空いていれば差はありません。
  • 試作は素朴な Python 実装のため、中央値(ふつうの応答)は 1〜2ms 遅くなります。毎回わずかな計算コストを払って、最悪ケースのスパイクを減らすというトレードオフです。本番品質の実装(C++)ならこのコストは消える見込みですが、まだ測っていないので断言はしません。
  • 比較相手は vLLM の既定の追い出しであって、世界最強のキャッシュ手法ではありません。

いちばん書きたいのは「一度、負けた」こと

この実験、最初は完全な失敗に見えました。シミュレーションではこの方策は既定よりヒット率 +6〜8%。ところが実機に組み込むと、差はほぼゼロ。どの負荷で測っても互角でした。

「シミュレーションの勝ちは幻だったのか」と一度は諦めかけました。しかし、パッチ自体は正しく動いています。そこで、TANREN の中核である「失敗の瞬間を計測する」という診断のやり方を、今度は自分の実験そのものに向けました。

flowchart TD
    A["シミュでは +6〜8%"] --> B["実機では差ゼロ"]
    B --> C["診断: 失敗の瞬間を計測する"]
    C --> D["ズレ1: 測る場所<br/>負荷の刻みが粗く、効果が集中する<br/>『膝』を跨いで素通りしていた"]
    C --> E["ズレ2: 検証器の物理<br/>vLLM の既定は素の LRU より賢く、<br/>シミュの『既定相当』は本物と違った"]
    D --> F["膝の位置を実機で合わせ直し、<br/>指標を平均からテール(p99)に変える"]
    E --> F
    F --> G["勝ちが現れる: p99 −13〜16%"]
    style B fill:#fee2e2,stroke:#b91c1c
    style G fill:#d1fae5,stroke:#059669

突き止めたのは、地味ですが決定的な 2 つのズレでした。

  1. 測っていた場所がズレていた。 この方策の勝ちは、キャッシュがちょうど足りなくなる境界(膝)に集中するという尖った性質を持っています。最初の測定は負荷の刻みが粗く、その膝を跨いで素通りしていました。
  2. 検証器(シミュレータ)の挙動が本物と微妙に違った。 シミュレータは単純な追い出しを仮定していましたが、vLLM の実物は既定の時点で素の LRU より賢く、古いブロックを捨てる順序に工夫があります。「既定相当」と呼んで比較していた相手が、実は本物の既定ではなかったのです。

膝の位置を実機で合わせ直し、見る指標を平均からテールに変えたところで、勝ちが現れました。方策は最初から効いていて、「どこを・何で測るか」を間違えていただけでした。

教訓: 方策を新しい環境へ移すときは、式をコピーするだけでは足りません。移した先のエンジンの追い出しの仕組みそのものを検証器に写し、勝ちが出る動作点に合わせ、平均ではなく分布の裾まで見る。この教訓は、次の第4話でそのまま活きてきます。

何の役に立つのか

会話型の LLM サービスは、いま最も使われている LLM の形です。混雑した本番サーバでテールの応答を 1 割強縮め、GPU の再計算を減らすことは、そのまま必要な台数とコストに効いてきます。しかも成果物は読める数十行のコードで、モデルは無変更、1 行でロールバックできます。安全に追加できる改善です。

配布の形も現実的だと考えています。ゼロから独自エンジンを作るのではなく、既存の OSS(vLLM / LMCache 等)に「追い出しの判断部分」だけを差し込むプラグインとして提供する。既存のエコシステムに乗る形です。

限界も書いておきます

  • 万能の高速化ではありません。 触ったのは LLM 効率化の数あるレバーのうち「prefix キャッシュの追い出し」という一点で、それも二次的な調整項目です。「LLM が何倍も速くなる」という話ではありません。
  • 効果は条件付きです。メモリに負荷のかかった会話型サーバのテールに効き、空いていれば効きません。
  • RAG(文書検索つき)には、この方式は効きません。 実データ(RAGPulse)で確認済みです。RAG は毎回異なる文書の組み合わせを使うため、prefix キャッシュに乗らないのです。ただし、文書を 1 個ずつ独立にキャッシュする別方式なら理論上限が約 2 倍になることも実測しており、そちらは今後の課題です。
  • 比較相手は既定の追い出しです。ARC / LFU 系や最新の文書キャッシュ手法との実機比較はこれからです。

勝てるところで勝ち、勝てないところは「勝てない」と実測で言う——それがこの連載の方針です。

プロトコル(事実チェック用)

  • 実機: NVIDIA GB10 / vLLM 0.19.1 / Llama-3.2-1B。--num-gpu-blocks-override で負荷を決定論的に再現。
  • データ: ShareGPT の実会話。進化に使っていない区間を、両方式に完全同一の列で再生。
  • 膝(blocks 2500〜3000)× シード: ヒット率 +0.4〜2.0pt、p99 TTFT −13〜16%。代償: 中央値 TTFT が試作 Python 実装のオーバーヘッドで 1〜2ms 悪化。
  • 第1話・第2話のチャンピオンコードと検証器は GitHub で公開中です。

連載の次回は、同じ仕組みで LLM サービングの心臓部——リクエストの処理順を決めるスケジューラ——を進化させ、重負荷でテール応答を半減させた第4話です。プロジェクト全体像に戻る。

この記事をシェア

関連記事