ステータス:ドラフト(2026-11-15)、実測。 本稿の数値は2026-11-15に筆者が直接測定した値です。Kubernetes v1.34、Dynamo v1.6.2の
vllm-runtime:1.6.2イメージ(vLLM 0.31.0)、モデルQwen3-Embedding-0.6B、8GB級ノートPC用GPU1基です。比較対象は汎用製品ではなく筆者が自作したサーバーの1つであるため、数値をそのまま他のサーバーに移行することはできません。 作成ルール:実測データを基本とし、測定できなかったものは「未実測」、確認できなかったものは「未確認」と表示します。
埋め込みサーバーをDynamoに移行してみる
目次
1. なぜ埋め込みなのか
第3部と 第4部ではテスト用チャットモデルで測定しました。今回は実際に使用しているサービスにDynamoを適用してみました。しかし、使用しているサービスを詳しく見るとチャットモデルではありませんでした。検索(RAG)に使用する埋め込み、再ランク付け、そして音声認識を1つのプロセスが1枚のGPUで処理する、自作のFastAPIサーバーでした。
このサーバーではDynamoの代表機能が役に立ちません。埋め込みはトーク数を生成しないため、プレフィックスキャッシュも、KVルーティングも、分離サービングも該当しません。Dynamoのコードも同じことを示しています。埋め込みワーカーのヘルプには、KVイベントとKVルーターの登録をスキップすると記載されています(v1.6.2、 components/src/dynamo/vllm/backend_args.py).
そのため、問いが変わります。 自作サーバーを撤去してDynamo埋め込みワーカーに置き換えることができるか、そしてそうした場合何が変わるのかということです。
2. 構成
DynamoのvLLMバックエンドは --embedding-worker 引数により埋め込み専用ワーカーになります。vLLM側には --runner pooling を併せて渡します。
command: ["python3", "-m", "dynamo.vllm"]
args:
- --model
- Qwen/Qwen3-Embedding-0.6B
- --embedding-worker
- --runner
- pooling
- --max-model-len
- "1024"
- --gpu-memory-utilization
- "0.4"
グラフはフロントエンド1つとワーカー1つです。モデルは既存サーバーが使用しているものと同じリビジョンをあらかじめ取得しておき、ワーカーは HF_HUB_OFFLINE=1 で起動しました。 --max-model-len 1024 は既存サーバーの入力制限に合わせた値です。
比較の手順は以下の通りです。
- 既存サーバーにリクエストを送信し、ベクトルとレイテンシを収集します(3回)。
- 既存サーバーを停止し、同じGPUにDynamoグラフをデプロイします。
- 同じリクエストをDynamoフロントエンドに送信します(3回)。
- グラフを停止し、既存サーバーを復旧させます。
リクエストは文書40件(韓国語と英語、1文書あたり平均84トークン)を8件ずつまとめたものです。既存サーバーのクライアントが実際に8件ずつまとめて送信するためです。レイテンシはシーケンシャル30回と並行度4で40回測定し、3回繰り返しのの中央値を使用します。
3. ベクトルは同一だった
| 項目 | 既存サーバー | Dynamo埋め込みワーカー |
|---|---|---|
| 次元 | 1024 | 1024 |
| 最初のベクトルの長さ(L2) | 1.0 | 1.0 |
| 文書40件のトークン数合計 | 3,375 | 3,375 |
| 文書ごとのコサイン類似度 | 最小0.9996 · 中央0.9998 · 最大0.9999 |
トークン数が正確に一致し、ベクトルは小数点第4位で分かれます。既存サーバーはtransformersで直接推論し、Dynamo側はvLLMで推論するため、演算順序と精度処理が異なります。検索順位を変えるほどの差であるかは別途測定していません(未実測)。インデックスを作り直さずに混用してよいかは、この数値だけでは断定できません。
4. そのままでは置き換えられなかった
両方のサーバーとも /v1/embeddings を開きます。名前が同じであるためアドレスを変更するだけで済みそうに見えますが、同じリクエストに対する応答が異なっていたものが4つありました。
| リクエスト | 既存サーバー | Dynamo埋め込みワーカー |
|---|---|---|
dimensions: 256 |
200、256次元 | 500 |
input_type: "query" (クエリ用インストラクション) |
200、インストラクションを付与して28トークン | 200, 無視して9トークン |
| 制限を超える長い入力 | 200、1,024トークンに切り詰め | 500 |
encoding_format: "base64" |
無視して配列 | base64文字列 |
dimensions. ワーカーのログに原因が残りました。vLLMがこのモデルをMatryoshka埋め込みをサポートしていないモデルと見なし、 dimensions が存在するとリクエストを拒否します。既存のクライアントはすべてのリクエストに dimensions を入れて送信します。値がモデルの本来の次元と同じであっても「存在してはならない」という検証にひっかかるため、クライアントを修正しない限りリクエストはすべて失敗します。vLLMにはモデルをMatryoshkaとして扱うオーバーライドオプションがあると認識していますが、本稿では検証していません(未実測)。
クエリ用インストラクション。 こちらの方がより危険です。エラーが発生しないためです。Qwen3-Embeddingはクエリの前にインストラクションを付与するように作られたモデルであり、既存サーバーは input_type や instruction フィールドを受け取ってその処理を代行します。Dynamo側は未知のフィールドを破棄し、文字列そのままを埋め込みます。同じクエリで両側が出力したベクトルのコサイン類似度は 0.82 でした。インストラクションを除いて比較すると0.9995です。アドレスを変更するだけで全てのリクエストが200で返ってくるものの、検索品質は静かに低下します。インストラクションはクライアントが文字列として付与して送信する必要があります。
長い入力。 既存サーバーは制限で切り捨て、vLLMは制限を超えると拒否します。どちらが正しいとは言い難いですが、もしクライアントが切り捨て処理をサーバーに任せていた場合、その処理を引き継ぐ必要があります。
エラーコード。 dimensions と長い入力はどちらもクライアントのリクエストが不正な場合ですが、フロントエンドは Internal server error とともに500を返しました。ワーカーのログにはvLLMの検証エラーが原因とともに残っていました。500はリトライ対象として扱うクライアントが多いため、このままだと修正不可能なリクエストを継続して再送し続けることになります。v1.6.2で見られた動作であり、最新版で変更されたかは未確認です。
5. レイテンシとスループット
同一GPU(8GB級)で測定した値です。バッチサイズ8のリクエスト1件あたりのレイテンシであり、3回繰り返しの1の中央値です。エラーは両者とも0件でした。
| シーケンシャル p50 | シーケンシャル p99 | 並行度4 p50 | 並行度4 p99 | 並行度4 スループット | |
|---|---|---|---|---|---|
| 既存サーバー | 54.3ms | 76.0ms | 225.9ms | 245.9ms | 141.0文書/s |
| Dynamo埋め込みワーカー | 36.9ms | 40.5ms | 77.4ms | 108.9ms | 395.8文書/s |
シーケンシャルレイテンシは0.68倍、並行度4のスループットは2.8倍です。繰り返しごとのスループットは既存サーバーが139.4・141.0・141.2、Dynamoが409.1・395.8・351.9文書/sでした。
並行度4でこのような結果になる原因は、既存サーバーの構造にあると考えられます。既存サーバーは推論をグローバルロック1つでラップし、一度に1つのリクエストのみを処理します。並行度4のレイテンシ(226ms)がシーケンシャル(54ms)の約4倍であることがその名残です。vLLMは同時に流入したリクエストを1つのバッチにまとめます。ただし、ロックを解除したりバッチ処理を無効にして別途測定したわけではないため、この説明は確認されたものではなく数値に合致した推測です。
この表を読む際に注意すべき点が3つあります。
- 条件が完全に同一ではない。 既存サーバーは同じカードに再ランク付けモデルと音声認識モデルも配置しています(カード使用量3.7GB)。Dynamoワーカーには埋め込みモデル1つのみを配置しました(3.1GB)。
- 既存サーバーには実際のトラフィックが混ざっている可能性がある。 測定中に他のリクエストをブロックしませんでした。
- 比較対象が自作サーバーである。 「Dynamoが2.8倍速い」ではなく、「リクエストを1つずつ処理するサーバーをバッチ処理エンジンに変更するとこれだけ変わる」と読み取るべきです。同様の違いはDynamoなしでvLLMをデプロイするだけでも発生する可能性が高いです。vLLM単体との比較は行っていません(未実測)。
6. 移行できないもの
既存サーバーが行っている処理は3つで、本稿で移行を試みたのはそのうちの1つです。
| 機能 | Dynamo v1.6.2のvLLMバックエンド | 本稿における扱い |
|---|---|---|
| 埋め込み | --embedding-worker |
実測 |
| 再ランク付け | 再ランク付けハンドラーはSGLangバックエンド側のコードでのみ確認しました | 未実測 |
| 音声認識 | リアルタイム文字起こしハンドラーがコードに存在します | 未実測 |
埋め込みだけを移行すると、サーバーが1つから2つに増えます。既存サーバーは3つのモデルを1つのプロセスに載せて8GBカード1枚を共有していますが、埋め込みを切り離すとそのカード上でプロセスが2つになり、フロントエンドポッドももう1つ生成されます。ワーカーが1台だけの場合、Dynamoフロントエンドが行う処理はリクエストをその1台に転送することだけです。
7. まとめ
- Dynamoの埋め込みワーカーは2つの引数(
--embedding-worker,--runner pooling)で起動し、同一モデルにおいて既存サーバーと同じベクトルを出力しました(コサイン類似度最小0.9995)。 - 同一GPU上でのシーケンシャルレイテンシは54msから37msへ、並行度4のスループットは141文書/sから396文書/sへ変化しました。差の大部分は、リクエストを1つずつ処理する構造とバッチ処理する構造の違いによるものと考えられます。
- アドレスを変更するだけの置き換えは失敗します。
dimensionsは500エラーになり、クエリ用インストラクションはエラーなく無視されてベクトルが異なり(コサイン類似度0.82)、長い入力は切り捨てられずに500エラーになります。 - 移行の事前に行うべき作業はクライアント側にあります。
dimensionsを除外し、インストラクションを文字列として付与し、長さを事前に切り捨て、500エラーをリトライしないように修正する必要があります。 - ワーカーが1台だけの埋め込みサービスにおいて、Dynamoがもたらす付加価値は大きくありません。得られたメリットはエンジンをvLLMに変更したことに起因する可能性が高く、それはDynamoなしでも実現できることです。
シリーズのこれまでの記事と合わせて見ると以下のようになります。KVルーティングはプレフィックスが重複するチャットリクエストにおいて効果を発揮します(第3部)。分離サービングには高速なネットワークと大規模なモデルが必要です(第4部)。埋め込みのように生成を伴わない処理にはどちらも該当せず、残る疑問はAPIコントラクトが一致しているかという点になります(本稿)。
参考
- NVIDIA Dynamo v1.6.2、
components/src/dynamo/vllm/backend_args.pyの--embedding-workerヘルプおよびembedding_worker_processes.py. - vLLMドキュメント、Pooling ModelsとOpenAI互換Embeddings API。
- Qwen3-Embeddingモデルカードのインストラクション(instruction)の使用法。
- マニフェスト、比較スクリプト(
embed_probe.py)、中央値のサマリーはリポジトリのdocs/experiments/2026-11-15-dynamo-embedding/にあります。