ステータス:ドラフト(2026-10-07)。 この記事の数値はすべて筆者が実際に測定した値です。環境は8GB級のGPU 1枚、0.6Bモデル、Dynamo 1.6.0.dev20261006(開発ビルド)です。パート6が読んだドキュメントはv1.5.0であるため、フラグやデフォルト値が異なる可能性があります。異なるGPUを持つ2台のサーバー間(1GbE級ネットワーク)は4.5節で測定しており、NVLinkおよびRDMA接続は 未実測 です。 作成ルール:実測データを基本とし、測定できなかったものは「未実測」と表記します。
目次
- 1. 環境と方法
- 2. フロントエンドを経由するコスト
- 3. KV認識ルーティング
- 4. 1枚のGPU上のprefill/decode分離
- 5. 遭遇した問題
- 6. 制限と未実測
- 7. 運用に向けたチェックリスト
- 参考文献
パート6はDynamoをドキュメントベースで読んでまとめた記事でした。ドキュメントに記載されている内容が実際にその通りであるかを確認するため、単一GPU環境に実際にインストールしてみました。この記事では4つの疑問に答えます。
- Dynamoフロントエンドを前に置くと、リクエストごとにどれくらい遅延が増加するのか。
- KV認識ルーティングは、同じプレフィックスを再度送信したリクエストをどれくらい同じワーカーにルーティングするのか。ラウンドロビンとどれくらい違うのか。
- ルーターがKVイベントを受信できなかった場合どうなるのか。
- prefillとdecodeを2つのプロセスに分けると、1枚のGPU上で最初のトークンはどう変わるのか。
- 異なるGPUを持つ2台のサーバーに分けると(1GbE級ネットワーク)、どう変わるのか。
短い答え。 フロントエンドのコストはサーバーが空いているときは約13 msであり、忙しいときは目立たなくなりました。KV認識ルーティングは同じプレフィックスを再送した60回のうち53〜60回を同じワーカーに送り、ラウンドロビンでは35〜43回でした。ルーターをイベントモードで起動したにもかかわらずワーカーがイベントを発行しない場合、ヒット率 que ランダムと同等の水準まで低下するにもかかわらずエラーは発生せず、ログに警告が1行残るだけでした。1枚のGPU上での分離サービングは、すべての条件において最初のトークンが4.7〜7.2倍遅くなりました。2つのワーカーが1枚のGPUを共有しており、KVの転送コストが加算されたためであり、これは分離サービングの利点を測定したものではありません。異なるGPUを持つ2台のサーバーに分けると、1,500単語の入力における最初のトークンが1.7秒、3,000単語では3.4〜3.5秒となり、単独サービング(44〜172 ms)の20倍以上に増加しました。増加した時間は、KVのバイト数をリンクのスループット(104 MB/s)で割った値と一致しました。
1. 環境と方法
- GPU:8GB級1枚。モデル:Qwen3-0.6B(レイヤー28、KVヘッド8、ヘッドサイズ128、bfloat16、トークンあたりKV 114,688バイト)。
- ソフトウェア:ai-dynamo 1.6.0.dev20261006(pip開発ビルド)、vLLM 0.30.0、NIXL 1.3.2。
- Kubernetesなしで1つのPython仮想環境上で実行し、ワーカーの検出は
--discovery-backend file(共有ファイル1つ)で代用しました。リクエストプレーンはTCPのデフォルト値です。 - ワーカーごとに
--gpu-memory-utilization 0.38、最大長4,096、即時実行(--enforce-eager)です。2つのワーカーが1枚のGPUを共有しているため、1つのワーカーが使用できるKV領域は14,816トークンでした。 - サーバー間テスト(4.5節)では、24GB級のコンシューマー向けGPUを搭載したサーバーをもう1台使用しました。このGPUは他のサービスと時分割で共有して使用するものであるため、ワーカーあたりのGPUメモリ比率を0.18(約4.4GB)のみ確保し、ドライバーがCUDA 12.8用であったためCUDA 12.9ビルドを別途インストールしました(5節)。2台のサーバーのポッド間におけるTCPのスループットは112〜113 MB/sでした。
- 測定クライアントは自作のものであり、リクエストは1回に1つずつ送信しました(分離サービングの表のみ同時実行数4があります)。スクリプトと生データはリポジトリの
docs/experiments/2026-10-07-nvidia-dynamo/にあります。
2. フロントエンドを経由するコスト
同じモデルをvLLMのみで起動した場合(solo)と、Dynamoフロントエンドの背後に配置した場合(dyn)を、入力512トークン、出力128トークンで3回ずつ比較しました。値は最初のエントリートークン生成時間(TTFT)の中央値です。
| 同時リクエスト | vLLM単独 | Dynamo配下 |
|---|---|---|
| 1 | 29〜39 ms | 42〜43 ms |
| 8 | 129〜130 ms | 129〜141 ms |
| 32 | 272〜274 ms | 272〜274 ms |
同時実行数1の中央値は30 msから43 msへ約13 ms増加しました。同時実行数8および32では区別がつかず、スループットも同じでした(同時実行数32で秒間1,507対1,509トークン)。フロントエンド(HTTP、トークン化、ルーティング判断)のコストはサーバーが空いているときのみ現れる固定コストであり、忙しいときは埋もれました。このモデルはトークンの処理が非常に高速であるため13 msが目立っていますが、大きなモデルであれば相対的な割合はさらに小さくなると予想されますが、これは 未実測 です。
3. KV認識ルーティング
3.1 設定
2つのワーカーを同じモデルで起動し、フロントエンドの --router-mode のみを変更しました。
| 名前 | フロントエンドオプション | ワーカー側 |
|---|---|---|
| ラウンドロビン | --router-mode round-robin |
KVイベントオフ |
| ランダム | --router-mode random |
KVイベントオフ |
| KV認識、近似 | --router-mode kv --no-router-kv-events |
KVイベントオフ |
| KV認識、イベントモードだがイベントなし | --router-mode kv |
KVイベントオフ |
| KV認識、イベントモード | --router-mode kv |
KVイベントオン(ZMQでパブリッシュ) |
負荷はプレフィックス(約1,500単語、合成)を12種類作成し、それぞれ6回ずつシャッフルして送信し、毎回末尾の24単語を変更しました。リクエストは72個であり、プレフィックスを2番目以降に送信した再リクエストは60個です。再リクエストの最初のトークンが最初に送信したリクエスト(約80 ms)の60%未満である場合、キャッシュにヒットしたとカウントしました。ヒットした場合は約15 ms、ミスした場合は約80 msでした。
3.2 結果
| ルーティング | 再リクエスト60回中のヒット数(3回) | 再リクエストの最初トークン平均 |
|---|---|---|
| ラウンドロビン | 35, 40, 43 | 34〜42 ms |
| ランダム | 44, 31, 45 | 32〜46 ms |
| KV認識、近似 | 60, 53, 60 | 15〜26 ms |
| KV認識、イベントモードだがイベントなし | 39, 44, 41 | 32〜38 ms |
| KV認識、イベントモード | 60, 60, 60 | 15 ms |
同じ構成を事前にもう一度実行した結果(ラウンドロビン 35, 40, 43、ランダム 37, 37, 41、近似 60, 60, 60、イベントなし 41, 44, 37、イベントモード 60, 60, 60)も傾向は同じでした。近似方式は初回実行で3回とも60回でしたが、再実行で1回53回となったため、自ら記憶する方式はイベント方式よりも少し不安定である可能性があると考えられますが、断定はできません。イベント方式は6回すべてが60回でした。
3.3 サイレント失敗
表の4行目は、この実験において最も重要な結果です。 --router-mode kv でイベントモードを有効にしたにもかかわらず、ワーカーがKVイベントを発行しない場合、ルーターのインデックスが空のままとなり、すべてのリクエストの重複度が0になり、負荷のみに基づいてワーカーが選択されるようになります。ヒット率はランダムの水準(39, 44, 41)まで低下したにもかかわらず、サーバーは正常に応答し、エラーも発生しませんでした。フロントエンドのログに以下の警告が1行出力され、イベントを発行するワーカーに変更して実行した際には出力されませんでした。
WARN ... KV-event publishing is disabled for a role that requires KV events
diagnostic_code="kv_event_publisher_disabled" ... requirement="cache_aware_routing"
ヒット率がランダムと同等であることを気付くには、ログの警告を確認するか、同じプレフィックスを再度送信したときに最初のトークンが短縮されるかを個別に測定する必要があります。ルーターを有効にしただけでヒット率を測定しない場合、この状態のまま運用されてしまう可能性があります。
3.4 シミュレータと実測の違い
このルーターのルールを純粋なPythonで実装して同じ負荷を実行してみると(最初から連続して重複するブロックが最も多いワーカー、タイの場合は負荷が少ないワーカー)、KV認識は60回中60回ヒットし、イベントを通知しない場合はラウンドロビンと同じ値になります。傾向は実測値と同じです。ただし、シミュレータのラウンドロビンは60回中49回であり、実測値(35〜43)よりも高くなっています。シミュレータのワーカーは一度処理したプレフィックスを永遠に記憶しますが、実際のワーカーはKV領域が14,816トークンであるため、12種類のプレフィックス(約18,000トークン)をすべて保持しきれず、古いものから追い出されます。KV認識ルーティングはプレフィックスをワーカーに分散させ、各ワーカーが6種類(約9,000トークン)のみを保持するようにするため、追い出されません。プレフィックスルーティングの利点には、「同じ場所に送る」ことに加えて「各ワーカーの作業セットが小さくなる」ことが含まれていることを意味します。
3.5 2台のサーバー前のルーター
異なるGPUを持つ2台のサーバーにワーカーを1つずつ配置し(KV領域は14,816トークンと27,088トークン)、同じ負荷をサーバー間のネットワーク経由で送信しました。再リクエスト60回中のヒット数は、ラウンドロビンが40, 47, 50、ランダムが47, 40, 52、KV認識の近似が60, 60, 60、KV認識のイベントモードが43, 60, 60でした。KV認識ルーティングは、2つのGPUが異なる環境においても同じ傾向でヒットしました。イベントモードの最初のイテレーションが43回だった理由は分析していません(イベント購読が確立される前のリクエストであった可能性がありますが、確認できていません)。サーバーごとに最初のリクエストの速度が異なり(44msと80ms)、「最初のリクエストの60%未満」というヒット判定がこのテストではあまり明確ではないという制限もあります。
4. 1枚のGPU上のprefill/decode分離
4.1 設定
プリフィル専用ワーカーとデコード専用ワーカーを2つのプロセスとして同じGPU上に立ち上げ(それぞれGPUメモリの38%、KV転送はNIXL)、1つのプロセスが両方の処理を行う構成と同じ負荷で比較しました。出力は64トークンであり、値は3回繰り返したTTFTの中央値です。
4.2 結果
| 入力(単語) | 同時リクエスト | 1プロセス(ms) | 分離(ms) | 倍率 |
|---|---|---|---|---|
| 1,500 | 1 | 78 | 406 | 5.2 |
| 1,500 | 4 | 190 | 1,359 | 7.2 |
| 3,000 | 1 | 171 | 812 | 4.7 |
| 3,000 | 4 | 398 | 2,442 | 6.1 |
すべてのセルにおいて分離の方が遅くなりました。ワーカーのKV転送メトリクスで見ると、同時実行数1、入力1,500単語の場合、1リクエストあたり平均196 ms(転送あたり119.3 MB、スループット608 MB/s)がかかり、同時実行数4で入力3,000単語の場合は平均711〜1,723 msでした。最初のトークンが1プロセスの場合よりも約330 ms遅くなった同時実行数1の場合、転送にかかった196 msはその大部分を占めています。残りの約130 msが何であるかは分解して測定していません(ズレ:未実測).
4.3 この結果の読み方
これは「分離サービングが遅い」のではありません。2つのワーカーが同じGPU1枚を共有しているため計算資源が増えておらず、分離が解決しようとする問題(長いprefillが他のリクエストのdecodeを妨げること)を解く仕組みが1つしかない構成です。そこにKVを移動するコストが追加されただけです。異なるGPUの2つのサーバーに分けた場合は4.5節で測定しました。NVLinkやRDMAで接続された環境でメリットがあるのかはこの記事では測定していません(未測定).
トークンあたりのKVが114,688バイトであるため、1,500トークンは約164 MiBとなります。第6編が引用したドキュメントの警告(遅い経路でKVを移動させるとTTFTが約98秒まで延びる可能性がある)と同じ傾向の結果です。ただし、その98秒は別のモデル、別の入力長、別の接続の値であり、この実験と規模を比較することはできません。608 MB/sで1,500トークンを移動させると約280 msであり、サーバー間のTCPのような0.1 GB/s台であれば1.5秒を超えます。
4.4 KV転送はなぜ遅かったのか
原因は特定できませんでした。転送方式を変更した結果は以下の通りです。
| UCX設定 | 結果 |
|---|---|
| デフォルト(制限なし) | 転送スループット 608 MB/s、初回のトークン 379〜384 ms |
| デバッグログを有効にしたデフォルト | 615 MB/s、初回のトークン 387〜389 ms |
TCP系に限定(UCX_TLS=tcp,cuda_copy,sm,self) |
621 MB/s、初回のトークン 381〜384 ms |
| TCPとデバイス内IPCを同時に許可 | 初回のトークン 383〜384 ms(スループット低下はログに残らず) |
デバイス内IPCのみを許可(UCX_TLS=cuda_ipc,cuda_copy,sm,self) |
エンジンが起動せず |
IPCのみを許可した際のエラーは no active messages transport ... cuda_ipc/cuda - no am bcopy と Destination is unreachable でした。NIXLのUCXバックエンドの初期化において、アクティブメッセージを送信できる転送が要求されますが、IPC転送はそれを提供していないと読み取れます。したがって、デバイス内転送を使用したいという理由で UCX_TLS をIPCのみに絞る設定はこの組み合わせでは使用できません。デフォルト設定でUCXが作成したインターフェースは tcp/eth0, tcp/lo, cuda_ipc/cuda でした。
転送方式を変更しても速度が変わらなかったため、ボトルネックはUCXの転送選択ではない可能性があります。どこがボトルネックなのか(リクエスト単位の直列化、ブロック単位の転送ディスクリプタ28個、コピー経路など)は分解して測定していません(未測定).
4.5 2台のサーバーに分けた分離サービング(異なるGPU、1GbE級ネットワーク)
一方のサーバーは前述の8GB級GPU、もう一方は24GB級のコンシューマー向けGPU(他のサービスと時分割)であり、ポッド間のTCPスループットは113 MB/sでした。ワーカーの検出は一方のポッド内のetcdで行い、KV転送はNIXLデフォルト(UCX)です。同一の負荷(出力64、各セルの繰り返し3回の初回のトークンの中央値)です。
| 構成 | 1,500単語 同時1 | 1,500 同時4 | 3,000 同時1 | 3,000 同時4 |
|---|---|---|---|---|
| 8GB級サーバー単体 | 78ms | 190ms | 172ms | 421ms |
| 24GB級サーバー単体 | 44ms | 92ms | 83ms | 181ms |
| 8GB級がprefill、24GB級がdecode | 1,752ms | 5,818ms | 3,533ms | 12,476ms |
| 24GB級がprefill、8GB級がdecode | 1,721ms | 5,637ms | 3,407ms | 12,222ms |
- サーバーを2つに分けると、単体サービングよりも20倍以上遅くなりました。 同時1において、1,500単語は78msが1.75秒に(24GB級単体の44msとは40倍)、3,000単語は172msが3.5秒に増加しました。
- 増加した時間はリンクによって説明がつきます。 3,000単語のリクエスト1つの転送は351.75 MB、3,374 ms、104 MB/sでした。トークンあたり114,688バイトに3,000トークンをかけると344 MBになるため、このリクエストは単語とトークンがほぼ1対1でした。prefillの83msに、351.75 MB ÷ 104 MB/s(約3.4秒)を足すと約3.4秒となり、測定値の3.4〜3.5秒と一致します。1,500単語は約176 MB ÷ 104 MB/sとなり、約1.7秒で測定値の1.72〜1.75秒と一致します。この構成における分離サービングのコストは、「KVバイト ÷ リンクスループット」として事前に計算することができました。
- どちらのGPUがprefillを行うかはほとんど関係ありませんでした(1,752対1,721、3,533対3,407ms)。時間がGPUの計算ではなくリンクに縛られており、GPUの速度差が見えませんでした。
- 同時4では、4つの転送が同じリンクを分割して使用しました。 1つの転送のスループットが26〜28 MB/sに低下し(4つの合計が約112 MB/s)、1つの転送に平均12.5〜13.1秒かかりました。リンクが1つしかない場合、同時実行数が増えるほど転送が列に並ぶため、TTFTのリクエスト数に対する増加がほぼ比例関係になりました。
- このテストは1GbE級のリンク、0.6Bモデル(トークンあたりのKVが114,688バイトでprefillが80msと安価な場合)であり、分離サービングが不利な側の極端な例です。リンクがはるかに高速である場合(RDMAなど)や、モデルが大きいためprefillがはるかに高コストである場合の損益は同様の方法で計算できますが、 測定していません(未測定).
5. 経験したトラブル
- メモリ制限によりプロセスが強制終了した。 2つのワーカーとフロントエンドを交互に起動していたところ、メモリ監視に引っかかり、エンジンコアプロセスが残ったまま次の実験が開始されてしまいました。実行前に毎回
pkill -9 EngineCoreでクリーンアップするようにスクリプトに記述しました。 - 最初のルーター再テストが無効だった。 2つ目のワーカーがモデルダウンロードのロック(
.lock)を取得できずに起動しなかったにもかかわらず、スクリプトは2つとも起動したと仮定して最後まで実行されました。3つの戦略すべてが1台のワーカーに対して実行され、その結果には意味がありませんでした。両方のワーカーがKV領域を確保したことを確認してからのみテストを開始するようにし、開始前にモデルを一度ダウンロードしておくように修正しました。無効だったログはそのまま残しました。 - UCXの転送方式を絞り込んだ最初の試みが無効だった。 フロントエンドがワーカーを発見できないままリクエストを送信したため測定に意味がなく、スクリプトを修正して再実行しました。
- 別のGPUサーバーに移行しようとしたが、ドライバーが一致しなかった(3つの問題を解決した)。 ドライバーが570(CUDA 12.8)のサーバーでは、デフォルトのインストール版(CUDA 13ビルド)のvLLM 0.30.0が
Error 804: forward compatibility was attempted on non supported HWでエンジンを起動できませんでした。595ドライバーのサーバーでは同じインストールがそのまま動作しました。解決までに3つの対応が必要でした。(1) vLLMのCUDA 12.9ビルドホイールとtorch 2.13.0+cu129を先にインストールし、NIXLもCUDA 12用に変更しました(12.9ランタイムが570ドライバーで動作するマイナーバージョン互換)。(2) それでも同じエラー804が発生しました。コンテナイメージのldconfigがイメージ内のCUDA 13互換libcuda.so.1をホストが注入したlibcudaよりも先に指していたためであり、ポッド内でその互換設定(/etc/ld.so.conf.d/00-compat-*.conf)を削除し、ldconfigを再実行するとGPUが認識されました。(3) エンジンがKV領域を決定した後の最初のサンプリングで、flashinferがCUDA 13ランタイムでコンパイルしたカーネルがcudaErrorInsufficientDriver (35)で異常終了したため、サンプラーをVLLM_USE_FLASHINFER_SAMPLER=0に設定して無効化しました。同じ仮想環境のインストール版をサーバー間で使い回すには、ドライバーがインストール版のCUDAビルドをサポートしているかどうかの確認から始める必要があります。 - 転送方式を絞り込んだところ、エンジンが起動しなかった。 4.4節のIPC専用設定です。エラー行を詳細に出力するようにスクリプトを修正して原因を読み解き、全ログを保存しました。
6. 制限事項と未測定項目
- NVLinkやRDMAで接続された環境は測定していません。分離サービングが有利になる条件(高速なリンク、大規模なモデル、長いprefill)は 未測定 です。2台のサーバーに分けたテスト(4.5節)は1GbE級のリンクが1つのみであり、サーバー間のテストにおけるルーターと分離サービングは異なる2つのGPUのみです。サーバー間でKVを転送する際のスループットの上限はリンク(112〜113 MB/s)でした。
- モデルが0.6Bと小さく、プレフィックスは合成(乱数単語)です。大規模なモデルや実際のドキュメントにおけるヒット率と最初のトークンの差は異なる場合があります。
- ルーターのテストはリクエストを1つずつ送信しました(同時実行数1)。リクエストが重なったときにルーターが負荷と競合をどのように調整するかは測定していません。
- 入力1,500は単語数であり、トークン数は個別にカウントしていません。サーバー間テストにおける3,000単語のリクエストは転送あたり351.75 MB(トークンあたり114,688バイトで割ると約3,070トークン)であり、単語とトークンがほぼ1対1でした。1枚のカード上のテストにおける転送あたりの平均119.3 MBは、同じ割合であれば約1,040トークン(1,500単語の7割強)に相当し、この対応関係と一致しません。その10秒間の平均に小さな転送が混ざっていた可能性を疑うのみで、原因は特定できていません(未測定)。そのため、4.2節の転送あたりの平均値はリクエスト1つのサイズとして解釈しない方が安全です。
- 繰り返しはセルごとに3回であり、標準偏差は算出していません。傾向は確実であっても、小数点以下は信用しない方が無難です。
- 開発ビルド(1.6.0.dev)で測定しており、第6編が参照したv1.5.0とはフラグやデフォルト値が異なる可能性があります。
7. 運用時のチェックリスト
- KV認識ルーティングを有効にした場合、 ワーカーがKVイベントを出力しているか を最初に対象のログで確認します。フロントエンドのログに
kv_event_publisher_disabledという警告が出ていないか確認します。 - ルーティングを変更した後は、「同じプレフィックスを再度送信したときに最初のトークンが減少するか」を直接測定します。エラーなしでランダムレベルまで低下する可能性があります。
- プレフィックスが1つのワーカーのKV領域よりも多様性に富んでいる場合、ルーティングがワーカー間でプレフィックスを分散させる効果が加わります。ワーカーのKV領域(サーバーログの
GPU KV cache size)とプレフィックスの合計を比較します。 - 分離サービングは、KVの移動経路(転送スループット)を個別に測定した上で検討します。この実験では、リンクが1GbE級であれば移動時間は計算値とほぼ一致しました(4.5節)。トークンあたりのKVサイズ(レイヤー数 × KVヘッド数 × ヘッドサイズ × 2 × バイト数)に入力長を掛けると1つのリクエストが移動させるバイト数が算出しでき、それを実際の転送スループットで割ると、最初のトークンに加算される時間が求まります。
- 実行前に、古いプロセス(エンジンコア)が残っていないか、モデルのダウンロードロックが残っていないかを確認します。
- サーバーごとにNVIDIAドライバーのバージョンが異なる場合、インストールしたvLLMとtorchのCUDAビルドをそのドライバーがサポートしているか最初に確認します。サポートしていない場合、サーバーのログではなくエンジンの起動段階でCUDAエラーにより異常終了します。
参考資料
- Dynamoリポジトリとドキュメント: https://github.com/ai-dynamo/dynamo
- NIXL: https://github.com/ai-dynamo/nixl
- vLLMの分離prefillとNIXLコネクタ: https://docs.vllm.ai/en/latest/features/disagg_prefill/ , https://docs.vllm.ai/en/latest/features/nixl_connector_usage/
- Qwen3-0.6B 設定(レイヤー、KVヘッド、ヘッドサイズ、データ型): https://huggingface.co/Qwen/Qwen3-0.6B/raw/main/config.json
- 本シリーズの第6回(分散推論とDynamo、ドキュメント基準)、第20回(測定方法)