第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 つです。
- モデルの重みには一切手を付けず、数十行の読める追い出し方策を進化で作った。
- それを vLLM の該当ファイル 1 つに組み込んだ(1 ファイルのパッチで、1 行の変更で元に戻せます)。
- 既定の追い出しと進化方策のそれぞれに、完全に同一の実会話列(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 / s1 | 74ms → 69ms(−7.9%) | 42.6% → 44.4%(+1.8pt) |
| 2000 / s2 | 71ms → 68ms(−3.9%) | 44.4% → 44.7%(+0.4pt) |
| 2500 / s1 | 72ms → 62ms(−13.4%) | 48.4% → 50.4%(+2.0pt) |
| 2500 / s2 | 72ms → 61ms(−14.2%) | 50.3% → 50.7%(+0.4pt) |
| 3000 / s1 | 72ms → 61ms(−15.4%) | 53.1% → 54.4%(+1.3pt) |
| 3000 / s2 | 71ms → 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 つのズレでした。
- 測っていた場所がズレていた。 この方策の勝ちは、キャッシュがちょうど足りなくなる境界(膝)に集中するという尖った性質を持っています。最初の測定は負荷の刻みが粗く、その膝を跨いで素通りしていました。
- 検証器(シミュレータ)の挙動が本物と微妙に違った。 シミュレータは単純な追い出しを仮定していましたが、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話です。プロジェクト全体像に戻る。