ステータス:下書き(2026-11-14)、実測。 この記事の数値は、2026-11-13に筆者が直接測定した値です。環境とリクエストは 第1部と同じです。Kubernetes v1.34、GPUサーバー3台(24GBクラスGPU 1枚、8GBクラスノートPC用GPU 2枚)、ノード間1GbE、Dynamo v1.6.2の
vllm-runtime:1.6.2イメージ(vLLM 0.31.0)、モデルはQwen3-0.6Bです。ルーターの引数はすべてデフォルト値です。作成ルール:実測データを基本とし、測定できなかったものは「未測定」、確認できなかったものは「未確認」と表記します。
ラウンドロビンとKVアウェアルーティング
目次
- 1. この記事で答える質問
- 2. マニフェストにおける変更点
- 3. 5つのバリエーション
- 4. 結果:ラウンドロビンは基準線と同等で、KVルーティングは異なる
- 5. 初回トークン時間とスループット
- 6. ワーカー3台でも同様か
- 7. 上限にどれほど近づけたか
- 8. ハッシュシードと近似モード
- 9. まとめ
- 参考
1. この記事で答える質問
第1部の結論は次のとおりでした。Serviceの背後にvLLMを複数台配置した場合、プレフィックスキャッシュのヒット率は「1台のワーカーが保持するプレフィックス数÷プレフィックスの種類数」で決まり、ワーカーを増やしても向上しません。この記事では、同じリクエストをDynamoに送信して3つの事柄を確認します。
- Dynamoを経由するだけで変化するものはあるか(ラウンドロビン)。
- KVアウェアルーティングを有効にすると、ヒット率と初回トークン時間がどれほど変化するか。
- ドキュメントが注意を促している2つの要素、すなわちハッシュシードとKVイベントを除外するとどうなるか。
2. マニフェストにおける変更点
基準線はDeploymentとServiceでした。Dynamoではそれら2つが DynamoGraphDeployment(DGD) 1つに置き換わります。オペレーターがこれを読み取り、ポッドとServiceを作成します。エンジン引数、イメージ、モデル、ノード配置はそのままにし、次の4点のみを変更しました。
| 変更点 | 基準線 | Dynamo |
|---|---|---|
| リソース | Deployment + Service | DGD 1つ(構成要素:フロントエンド1、ワーカー複数) |
| ワーカーの実行コマンド | python3 -m vllm.entrypoints.openai.api_server |
python3 -m dynamo.vllm |
| OpenAI APIを開く場所 | ワーカーごとに8000 | フロントエンドのみ8000、ワーカーはHTTPを開かない |
| ルーティング | Serviceが接続単位でランダム | フロントエンドの DYN_ROUTER_MODE |
KVアウェアルーティングに必要な設定は2つです。
# 프런트엔드
env:
- {name: DYN_ROUTER_MODE, value: kv}
# 워커 인자
- --kv-events-config
- '{"publisher":"zmq","topic":"kv-events","endpoint":"tcp://*:20080","enable_kv_cache_events":true}'
ワーカーはKVブロックが作成および削除されるたびにこのアドレスにイベントを送信し、フロントエンド内のルーターがそれを受け取って「どのワーカーがどのブロックを保持しているか」のマップを作成します。リクエストが到着すると、ルーターはリクエストの先頭部分と最も多く重複するワーカーを選択しますが、その際にワーカーの負荷も考慮します。
移行時に確認した事実を記録しておきます。
- etcd、NATS、Groveなしで起動しました。 ワーカーは自身の情報を
DynamoWorkerMetadataというリソースとしてKubernetes APIに書き込み、フロントエンドがそれを読み取ります。 - 同種の構成要素を複数配置できました。 ノードごとにリソース制限が異なるため、
type: worker構成要素をノード別に分割し、フロントエンドはそのワーカー群を1つのモデルのワーカーとしてまとめて処理しました。 - ワーカーの指標は9090から取得できます。 オペレーターによって挿入されるシステムポートです。
vllm:prefix_cache_hits_totalなどのエンジン指標もここからスクレイピングしました。 cached_tokensは、別途有効にしなくてもレスポンスに含まれていました。 vLLM単体のサーバーでは--enable-prompt-tokens-detailsが必要だった値です。- ワーカーがReadyになった後も、フロントエンドの
/v1/modelsにモデルが表示されるまでに数秒かかりました。負荷をかける前にこのリストを確認する必要があります。
3. 5つのバリエーション
| 名前 | 構成 | 検証内容 |
|---|---|---|
| 基準線 | Serviceの背後のvLLM(第1部) | 比較値 |
| ラウンドロビン | DGD、 DYN_ROUTER_MODE=round-robin |
Dynamoを経由するだけの効果 |
| KVルーティング | DGD、 DYN_ROUTER_MODE=kv、KVイベント、 PYTHONHASHSEED=0 |
主要機能 |
| シードなし | KVルーティングから PYTHONHASHSEED のみを除外 |
ハッシュシードは本当に必要か |
| 近似モード | DYN_ROUTER_USE_KV_EVENTS=false、ワーカーのイベントも無効化 |
イベントなしの推測のみでどこまで可能か |
リクエストは第1部と同じです。共有プレフィックス約2,000トークンに、リクエストごとに異なるテール約300トークンを付与し、1台のワーカーのKV容量を1GiB(リクエスト約3.95回分)に制限しました。プレフィックスの種類は2・7・12・24、同時リクエストは4・8・16とし、各点で3回実行してその中央値を使用しました。シードが同一であるため、すべてのバリエーションが全く同じリクエストを同じ順序で受け取りました。5つのバリエーションのすべての点においてエラーは0件でした。
ラウンドロビンのバリエーションの1点(プレフィックス2種・同時実行4)は、最初の試行を除外しました。直前に同じシードでテストリクエストを送信したため、プレフィックスがすでにキャッシュに存在していたためです。その点は残りの2回の中央値です。
4. 結果:ラウンドロビンは基準線と同等で、KVルーティングは異なる
リクエストのうちキャッシュにヒットした割合です(ワーカー6台、同時実行8)。
| プレフィックス種類 | Serviceの背後のvLLM | ラウンドロビン | KVルーティング |
|---|---|---|---|
| 2 | 95.0% | 95.0% | 97.5% |
| 7 | 55.5% | 53.5% | 83.0% |
| 12 | 32.1% | 29.2% | 67.9% |
| 24 | 17.5% | 17.5% | 43.3% |
入力トークンのうちキャッシュから取得された割合で見ても同様です。このリクエストでは2,371トークン中2,032トークンがプレフィックスであるため、0.857が達成可能な最大値となります。
| プレフィックス種類 | 同時実行 | Serviceの背後のvLLM | ラウンドロビン | KVルーティング |
|---|---|---|---|---|
| 2 | 4 | 0.814 | 0.814 | 0.857 |
| 7 | 4 | 0.431 | 0.385 | 0.788 |
| 12 | 4 | 0.241 | 0.262 | 0.664 |
| 24 | 4 | 0.135 | 0.126 | 0.429 |
| 2 | 16 | 0.814 | 0.814 | 0.835 |
| 7 | 16 | 0.425 | 0.442 | 0.779 |
| 12 | 16 | 0.268 | 0.237 | 0.695 |
| 24 | 16 | 0.141 | 0.148 | 0.431 |
ラウンドロビンは基準線と区別できません。 Dynamoのフロントエンドを手前に置くだけではキャッシュは有効になりません。リクエストをワーカーに均等に振り分けることと、ランダムに割り振ることは、プレフィックスキャッシュの観点からは同じことです。
KVルーティングは、プレフィックスが1台の容量を超える区間でヒット率を2倍から3倍に向上させました。 プレフィックスが2種の場合は差が小さくなります。第1部に記載したとおり、プレフィックスが1台に収まる場合、ルーティングによるメリットはほとんどありません。
ルーターが実際にリクエストを集約したかどうかは、「1つのプレフィックスが何台のワーカーにルーティングされたか」で確認しました。リクエストの nvext.extra_fields に worker_id を入れると、レスポンスに処理したワーカーの識別子が含まれて返されます。それを使用して集計しました。
| プレフィックス種類 | ラウンドロビン | KVルーティング |
|---|---|---|
| 2 | 6.00台 | 1.00台 |
| 7 | 6.00台 | 2.86台 |
| 12 | 6.00台 | 4.00台 |
| 24 | 5.92台 | 5.17台 |
(ワーカー6台、同時実行4、3回の中央値。)ラウンドロビンはすべてのプレフィックスをすべてのワーカーに送信しました。KVルーティングは、プレフィックスが2種の場合はプレフィックスごとに1台のみに送信し、プレフィックスが増えるにつれて分散しました。
5. 初回トークン時間とスループット
初回トークンまでの時間(TTFT)です。セルごとに p50 / p99 です(ワーカー6台)。
| プレフィックス種類 | 同時実行 | Serviceの背後のvLLM | ラウンドロビン | KVルーティング |
|---|---|---|---|---|
| 2 | 4 | 37 / 122ms | 41 / 125ms | 34 / 73ms |
| 7 | 4 | 63 / 184ms | 89 / 142ms | 40 / 129ms |
| 12 | 4 | 94 / 193ms | 106 / 140ms | 43 / 138ms |
| 24 | 4 | 101 / 254ms | 113 / 143ms | 64 / 149ms |
| 2 | 16 | 64 / 325ms | 74 / 332ms | 67 / 206ms |
| 7 | 16 | 142 / 1,353ms | 111 / 576ms | 72 / 240ms |
| 12 | 16 | 193 / 1,441ms | 190 / 646ms | 77 / 193ms |
| 24 | 16 | 205 / 1,508ms | 199 / 774ms | 109 / 242ms |
読み取るべきポイントは3つあります。
- フロントエンドを経由するコストは数msです。 キャッシュ条件が同じプレフィックス2種・同時実行4において、ラウンドロビンのp50は41ms、基準線は37msでした。同時実行8では64ms対47msに開きました。フロントエンドがトークン化を行い、ワーカーにリクエストを転送するステップが1つ増えるためです。
- ラウンドロビンはヒット率が同じであるにもかかわらず、テールレイテンシが短くなりました。 同時実行16・プレフィックス24種のp99は774msであり、基準線の1,508msの半分です。Serviceは接続ごとにランダムにポッドを選択するため、1台のワーカーにリクエストが集中する瞬間が生じるのに対し、ラウンドロビンは均等に振り分けるという違いに起因すると考えられます。ワーカーごとの瞬間的なキュー長を測定していないため、原因は推測です。
- KVルーティングはさらにテールレイテンシを短縮しました。 同時実行16において、p99はプレフィックス数に関わらず0.2秒前後です。ミスしたリクエストは2,371トークンすべてを計算するために同じGPU上の他のリクエストを押し出しますが、そうしたリクエスト自体が減少するためです。
スループット(秒間リクエスト数、ワーカー6台・同時実行16)も向上しました。
| プレフィックス種類 | Serviceの背後のvLLM | ラウンドロビン | KVルーティング |
|---|---|---|---|
| 2 | 18.9 | 19.1 | 21.1 |
| 7 | 14.1 | 16.0 | 20.1 |
| 12 | 13.9 | 14.7 | 19.2 |
| 24 | 13.3 | 14.3 | 17.6 |
GPUは同じままで、プレフィックス24種において32%多く処理しました。プレフィックスの再計算に費やされていた時間が減少した分です。
6. ワーカー3台でも同様か
GPU 1枚につきワーカー1台を配置した構成です(同時実行8)。
| プレフィックス種類 | キャッシュトークン比率:基準線 / ラウンドロビン / KV | TTFT p99:基準線 / ラウンドロビン / KV | 秒間リクエスト:基準線 / ラウンドロビン / KV |
|---|---|---|---|
| 2 | 0.840 / 0.840 / 0.848 | 165 / 171 / 83ms | 17.0 / 17.3 / 20.6 |
| 7 | 0.487 / 0.475 / 0.690 | 853 / 413 / 145ms | 12.4 / 13.6 / 18.5 |
| 12 | 0.283 / 0.291 / 0.474 | 1,125 / 679 / 152ms | 11.2 / 12.5 / 17.2 |
| 24 | 0.143 / 0.141 / 0.252 | 1,216 / 830 / 202ms | 10.7 / 11.8 / 16.6 |
傾向は同じです。プレフィックス24種でスループットが55%増加しました。ただし、ワーカーが3台しかないため保持できるプレフィックスの総量が半分になり、それに伴いヒット率も6台構成よりも低くなります。
負荷が容量を超える点も1つありました。ワーカー3台・同時実行16・プレフィックス24種において、KVルーティングのTTFT p50は279ms、p99は962msでした。基準線(563ms、3,060ms)よりは優れていますが、他の点のように0.2秒以内に収まることはありませんでした。ルーティングは無駄を削減するだけであり、不足している計算リソースを生み出すわけではありません。
7. 上限にどれほど近づけたか
第1部で計算した理論上の上限は「ワーカー数×1台が保持するプレフィックス数÷プレフィックス種類数」でした。プレフィックスをワーカー間で重複しないように分割して保持すると仮定した値です。
| 構成 | プレフィックス種類 | 上限 | 実際の درخواست 単位のヒット率(同時実行 4 / 8 / 16) |
|---|---|---|---|
| ワーカー6台 | 7 | 100% | 93 / 83 / 91% |
| ワーカー6台 | 12 | 100% | 80 / 68 / 82% |
| ワーカー6台 | 24 | 99% | 52 / 43 / 52% |
| ワーカー3台 | 7 | 100% | 77 / 82 / 90% |
| ワーカー3台 | 12 | 99% | 53 / 58 / 64% |
| ワーカー3台 | 24 | 49% | 29 / 30 / 28% |
デフォルト値のKVルーティングは、キャッシュが逼迫している区間で 上限の半分から3分の2の間を記録しました。ランダム分散よりは2〜3倍優れていますが、容量を余すことなく活用する構成には及びません。
その理由は第4部の表に表れています。プレフィックス24種において、1つのプレフィックスが平均5台のワーカーに送信されていました。ルーターは重複だけでなくワーカーの負荷も考慮するため、重複しているワーカーが忙しい場合は別のワーカーに送信します。そのワーカーはプレフィックスを新しく計算して保持するため、その過程で別のプレフィックスを追い出します。キャッシュが逼迫するほど、この追い出しが連鎖的に発生します。
Dynamoのドキュメントには、重複と負荷の比率を変更するルーターの引数があります。この記事ではデフォルト値でのみ測定しており、その引数を変更することで上限にさらに近づけることができるかどうかは未測定です。
8. ハッシュシードと近似モード
ドキュメントとサンプルでは、ワーカーに PYTHONHASHSEED=0 を与えるよう記載されています。vLLMのブロックハッシュがPythonのハッシュを経由するため、シードがプロセスの異なる場合に同じプレフィックスがワーカーごとに異なるハッシュになってしまうというのがその理由です。シードを抜いて再計測してみました(ワーカー6台、同時実行8、キャッシュトークン比率)。
| プレフィックスの種類 | ラウンドロビン | KVルーティング | シードなし | 近似モード |
|---|---|---|---|---|
| 2 | 0.814 | 0.835 | 0.848 | 0.848 |
| 7 | 0.439 | 0.702 | 0.729 | 0.638 |
| 12 | 0.247 | 0.573 | 0.547 | 0.480 |
| 24 | 0.149 | 0.361 | 0.343 | 0.245 |
シードを抜いても結果は同じでした。 4点の差は0.03以内であり、方向も交錯しています。反復間の揺らぎと区別がつかない大きさです。少なくともこの実験とこの構成では、シードがなくてもルーティングが崩れませんでした。理由は未確認です。ランタイムがシードを自ら決定するのか、ルーターがワーカーのハッシュに依存しない方法があるのかは、コードから確認できませんでした。ドキュメントが要求する設定であり、追加のコストもかからないため、結果が同じだったという理由だけで外すことは推奨しません。
近似モードは中間です。 このモードではワーカーはイベントを送信せず、ルーターは自分が送信したリクエストの記録から「そのワーカーにまだこのプレフィックスが存在するはずだ」と推測します。プレフィックス7種ではKVイベントを使用した場合と大きな差はありませんでしたが(0.638対0.702)、24種では0.245となり、ラウンドロビン(0.149)との差が半分に縮まりました。キャッシュが枯渇してブロックが頻繁に追い出されると、ルーターの推測と実際が食い違うためだと思われます。イベントポートを開きにくい環境で使用できる次善の策程度として読み取ればよいでしょう。
9. まとめ
- Dynamoを前段に置くだけではプレフィックスキャッシュは有効になりません。ラウンドロビンのヒット率はServiceの後ろのvLLMと同じでした。
- KV認識ルーティングは、プレフィックスの総量がワーカー1台の容量を超える区間で効果を発揮します。ワーカー6台において、リクエスト単位のヒット率はプレフィックス7種で56%→83%、12種で32%→68%、24種で18%→43%になり、同時実行16のTTFT p99は1.3〜1.5秒から0.2秒台になりました。
- プレフィックスが1台にすべて収まるサービスであれば、得られるものはほとんどありません。プレフィックス2種でのヒット率の差は2〜4%pでした。その場合に支払うコストは、フロントエンド1段分の数msと、 第2部で見たインストール・権限の負担です。
- デフォルト値では理論上の上限の半分から3分の2を出力します。ルーターの引数の調整は未計測です。
- ハッシュシードを抜いても結果は同じでした(理由は未確認)。近似モードはキャッシュが枯渇するほど効果が低下しました。
この測定の限界は第1部と同様です。プレフィックスを均等に抽出し、リクエストの長さがすべて同じで、モデルが小さい点です。大きなモデルでは、外れたリクエスト1件あたりの計算がはるかに長いため、同じヒット率の差がより大きな時間差として現れるはずですが、それは本記事で測定した値ではありません。フロントエンドを複数台配置した際、ルーターが同じマップを参照しているかも未計測です。
参考
- NVIDIA Dynamo v1.6.2 ドキュメント、KV Cache Routing と Router の設定。
- マニフェスト、負荷ツール、グリッドのログと中央値の要約はリポジトリの
docs/experiments/2026-11-13-dynamo-adoption/にあります。 - KVイベントとルーターの原理は LLM KVキャッシュ解剖 第4部にあります。