ステータス:ドラフト(2026-10-27)、プーリング・マルチモーダル実測、並列化未測定。 プーリングの測定は8GBクラスのノートPC用GPU(Ada世代)1基、vLLM 0.31.0を使用して2026-10-27に行いました。並列化とマルチモーダルは未測定であり、公式ドキュメント(0.32.0)および論文で確認できた内容のみを記載しています。LoRAマルチサービングの実測は「トス ML/AI Infra 準備」の第15話にあります。 2026-10-27 追加。 承認を受けてvLLM 0.32.0イメージとQwen3-VL-2B-Instruct(Apache-2.0、safetensors 4.26GB)を取得し、セクション2-1のマルチモーダル実測を追加しました。初版では「未測定」だった内容です。 執筆ルール:実測データを基本とし、測定できなかったものは「未測定」、確認できなかったものは「未確認」と表示します。
目次
- 1. 4つの並列化:何を分割するのか
- 2. マルチモーダル入力
- 2-1. 2026-10-27の実測:8GB環境で2Bのビジョン言語モデルを動かす
- 3. プーリング:埋め込みと再ランク付けも同じエンジンで
- 4. LoRAマルチサービング
- 5. 確認できなかったこと
- 参考
1. 4つの並列化:何を分割するのか
| 方式 | オプション | 分割するもの | 通信 | 使用するタイミング |
|---|---|---|---|---|
| テンソル並列(TP) | --tensor-parallel-size |
1つの層の行列をGPUごとに分割 | 層ごとにall-reduce | モデルがGPU 1基に収まらない場合、1つのサーバー内 |
| パイプライン並列(PP) | --pipeline-parallel-size |
層を前後に分割し、GPUごとに数層ずつ | ステージ間の活性値のみ | 複数サーバーにまたがる場合、NVLinkがない場合 |
| データ並列(DP) | --data-parallel-size |
分割せずモデルを丸ごと複数コピー | ほぼなし(MoEにはあり) | スループットを向上させる場合 |
| エキスパート並列(EP) | --enable-expert-parallel |
MoEのエキスパートをGPUごとに | トークンをエキスパート側にall-to-all | 大規模なMoEモデル |
- TPは通信頻度が高くなります。 vLLMはMegatron-LM(Shoeybiら、2019)の方式で行列を分割しますが、この方式はTransformer層の順伝播ごとにall-reduceを2回(アテンション後とMLP後)行います。そのため、GPU間がNVLinkのように高速な場合に大きなメリットがあります。公式ドキュメントでも、NVLinkがないGPU(例:L40S)ではTPの代わりにPPを推奨しています。
- PPは通信が少ないですが、バブル(アイドル時間)が発生します。 前のステージが完了しないと次のステージが動作しないため、1つのリクエストだけを見るとGPUが交互に待機状態になります。複数のリクエストを流し込むことでバブルを埋めます。ドキュメントでは、サーバー内はTP、サーバー間はPPで分割することを推奨しています(サーバーあたりのGPU数 = TP、サーバー数 = PP)。
- DPはKVキャッシュも別々になります。 エンジンごとにプレフィックスキャッシュが独立しているため、同じプレフィックスを同じエンジンに送信しないとキャッシュが活きません。KVを確認してエンジンを選択するルーティングがDynamoの主要なテーマでした。DPにおいて、
--max-num-seqsはエンジンごとに適用されますが、--max-num-queued-reqsのようなキューの制限はサーバー全体に適用される点もドキュメントに記載されています。 - MoEは組み合わせて使用します。 DeepSeekなどのモデルでは、アテンションはDP、エキスパート層はEPで分割する構成がドキュメントで紹介されています。
モデルが収まるか確認する際は、起動ログの2行を確認します。 GPU KV cache size: N tokens と Maximum concurrency for M tokens per request: Kx です。このシリーズの実験サーバーは「40,208トークン、4,096トークンリクエスト基準で9.82倍」でした。この並行性が目標より低い場合、GPUを増やすようドキュメントで案内されています。
GPUが1基であるため、4つの方式すべて未測定です。同系統のGPU 2基を1GbEで接続してKVを転送した際の実測(分散サービングが20倍以上遅かった)が「トス ML/AI Infra 準備」の第24話にあるため、低速な接続でGPUを接続する際にかかるコストについては、その記事で代わりとします。
2. マルチモーダル入力
vLLMは画像・動画・音声を処理するモデルを、同じOpenAI互換APIで提供します。チャットメッセージの content に image_url, video_url, input_audio の断片を挿入すると、APIサーバーのプロセッサがメディアをダウンロードして前処理を行い、モデルのエンコーダーに送信します。画像1つはプロンプト内で複数のプレースホルダー・トークンに変換され、プレフィックスキャッシュはこのプレースホルダーブロックのハッシュに画像のハッシュを混ぜ合わせ、同じ画像同士でのみキャッシュを共有します(第2話のプレフィックスキャッシュ設計ドキュメント)。
運用においてパフォーマンスよりも先に対処すべきなのはこちらです。
- SSRF:リクエストが指定したURLをサーバーが代わりにダウンロードするため、サーバーがアクセスできる内部アドレスをリクエスト元が叩く可能性があります。ドキュメントでは、
--allowed-media-domainsで許可するドメインを指定し、VLLM_MEDIA_URL_ALLOW_REDIRECTS=0でリダイレクトを禁止するよう推奨しています。コンテナ内で実行している場合はさらに重要であると記載されています。 - メモリ予約:vLLMはモデルが処理できる最大サイズのメディアを基準に、エンコーダーのメモリを事前に確保します。使用しない入力は
--limit-mm-per-prompt '{"image":0,"video":0}'のように0に設定すると、その分をKVに割り当てることができ、サイズヒント(count,width,height,num_frames)によって予約を減らすことも可能です。 - CPUメモリ:同じメディアを再度処理しないためのプロセッサキャッシュがデフォルトで4GiB(
mm_processor_cache_gb)確保されます。コンテナのメモリ制限を設定する際にこの分を追加する必要があり、メリットがない場合は0にして無効化します。 - 0.32.0では、信頼できない画像をラスター形式の許可リストに載っているものだけに制限するように変更されました(EPS・PostScript・WMFの拒否、リリースノート基準)。
初版ではビジョン言語モデルを新しく取得する必要があったため未測定でしたが、承認後に以下のセクション2-1で測定しました。
2-1. 2026-10-27の実測:8GB環境で2Bのビジョン言語モデルを動かす
- 環境:8GBクラスのノートPC用GPU(Ada世代)1基、vLLM 0.32.0、Qwen3-VL-2B-Instruct(bf16、重み4.08GiB)、
--max-model-len 4096. - 画像:PILで描画したPNGをデータURLとして入力しました(外部アドレスは取得していません)。白背景に色のついた四角形1つと4桁の数字。サイズ448×448と896×896をそれぞれ5枚。
- 質問:1枚の画像に対して3回。①「数字は何ですか」(最初)、②同じ質問を再度、③「四角形の色は」。貪欲デコーディング。
まず、起動させる
| 設定 | KVの残りメモリ | KVの空き | 結果 |
|---|---|---|---|
デフォルト(--gpu-memory-utilization 0.85) |
-0.07GiB | 起動失敗 「No available memory for the cache blocks」 | |
--limit-mm-per-prompt '{"image":1,"video":0}' のみ追加 |
-0.07GiB | 同様に失敗 | |
| メモリ比率 0.92 | 0.47GiB | 4,352トークン(4,096トークンリクエスト1.06個分) | 起動成功 |
0.92 + サイズヒント {"image":{"count":1,"width":896,"height":896},"video":0} |
2.13GiB | 19,904トークン(4.86個分) | 起動成功 |
vLLMは起動時に「モデルが受け取ることのできる最大サイズ画像」でエンコーダーを1回実行し、メモリを測定します。ログには、エンコーダーキャッシュの予算を16,384トークンに設定したと記録されています。重み4GBにその予算まで加わると、8GB環境ではKVの空き領域が残りませんでした。画像の枚数を減らすだけでは意味がなく、実際に受け取る画像のサイズを教えることで、KVが4.5倍になりました。小型GPUでビジョン言語モデルを起動する際に真っ先に調整すべきパラメータです。
結果(サイズヒントサーバー)
| 項目 | 448×448 | 896×896 |
|---|---|---|
| プロンプトトークン(画像 + 質問) | 220 | 808 |
| 新規画像のTTFT中央値 | 67.9ms | 238.0ms |
| 同じ画像、同じ質問を再度 | 31.8ms(キャッシュ208トークン) | 32.7ms(キャッシュ800トークン) |
| 同じ画像、異なる質問 | 32.1ms(キャッシュ192トークン) | 33.4ms(キャッシュ784トークン) |
- 画像トークンは面積に比例しました。 辺の長さを2倍にすると、トークン数が3.7倍、初トークンまでの時間が3.5倍になりました。
- 同じ画像を再度送信すると、エンコーダーとプリフィルをスキップします。 プレフィックスキャッシュは画像ハッシュをブロックハッシュに混ぜ合わせて同じ画像同士でブロックを共有し(質問が異なっていても画像までの前半部分はヒット)、プロセッサキャッシュの指標である
vllm:mm_cache_hits_totalは、30回中20回(2回目・3回目の使用)のヒットをカウントしました。 - 正解率:色 10/10、4桁の数字 0/10。 数字について、6082を「6002」、5435を「543」、9532を「932」と読み取るなど、1文字を変更したり欠落させたりしました。2Bモデルの限界なのか前処理のせいなのかを切り分けることができなかったため、未確認です。
SSRF:サーバーが代わりにダウンロードするか
| リクエストの画像アドレス | デフォルトサーバー | --allowed-media-domains example.com |
|---|---|---|
http://127.0.0.1:8000/health(サーバー自身) |
400 「Failed to load image: cannot identify image file」(5.5ms) | 400 「The URL must be from one of the allowed domains」(2.6ms) |
http://127.0.0.1:8000/metrics |
400、同じメッセージ | 400、同じ拒否 |
file:///etc/passwd |
500 「Cannot load local files without --allowed-local-media-path」 |
同上 |
デフォルトサーバーのメッセージは「画像ではない」です。すなわち、 サーバーがその内部アドレスに対して実際にリクエストを送信し、レスポンスを受け取ってきたことを意味します。レスポンスの内容は返されませんでしたが、リクエスト元はこのサーバーを介して内部ネットワークを叩き、応答時間やエラーから何が公開されているかを調べることができます。ドメインを指定すると、受け取る前に拒否されました。ローカルファイルはデフォルトでブロックされていました。画像URLを受け取るサービスであれば、 --allowed-media-domains は選択ではなく必須です。
3. プーリング:埋め込みと再ランク付けも同じエンジンで
生成を行わず、最後の隠れ状態を取得して使用するモデルをvLLMでは プーリングモデルと呼びます。 --runner pooling で起動すると、生成の代わりに /v1/embeddings, /score, /rerank, /classify, /pooling を提供します。因果モデルは最後のトークン(LAST)、BERT系モデルは最初のトークン(CLS)をデフォルトで使用します。公式ドキュメントには、「プーリングモデルのサポートは利便性のためのものであり、他の方式より速いという保証はない」とあらかじめ記載されています。
方法
- 埋め込み:Qwen3-Embedding-0.6Bを
--runner poolingで起動し、ランダムな単語120個(約127トークン)からなる文章64個を1つのリクエストとして送信しました。6回のうち最初の1回はウォームアップ。 - 再ランキング: Qwen3-Reranker-0.6B は本来「yes」と「no」トークンのスコアで関連性を出す生成モデルです。vLLMのサンプルの方法通り
--hf-overrides '{"architectures":["Qwen3ForSequenceClassification"],"classifier_from_token":["no","yes"],"is_original_qwen3_reranker":true}'とサンプルのチャットテンプレートで分類モデルのように立ち上げました。質問1つと短い文書32個(6種を繰り返し)を/rerankに送信しました。
結果
| タスク | 実行したリクエスト | レイテンシ中央値(5回) | スループット | 備考 |
|---|---|---|---|---|
| 埋め込み | 文64個 × 約127トークン | 371.8ms | 秒間172個(約21,900トークン) | 次元 1,024 |
| 再ランキング | 質問1 + 文書32個 | 33.9ms | 秒間944個 | 一致する文書が 0.993 で1位 |
- 埋め込みのトークン処理速度(秒間約21,900トークン)は、同じGPUでの生成モデルのプリフィル速度(第2回、秒間約18,000トークン)と同等でした。プーリングはプリフィルしか行わない処理だからです。
- 再ランキングは「サービングエンジンは共通接頭辞をどのように書き換えるか」という質問に対し、接頭辞キャッシュの文を 0.993 で最上位に挙げました。再ランキング用に学習されたモデルであるため、この処理に優れています。
- プーリングサーバーもKVキャッシュを4.3GiB(40,272トークン)確保しました。因果アテンションプーリングではチャンクドプリフィル(chunked prefill)と接頭辞キャッシュが動作すると機能表に記載されているため、その領域として使用されます。埋め込みのみを行うサーバーであれば、
--gpu-memory-utilizationを下げてGPUを他の処理と共有できます(未計測)。 - セキュリティ文書によると、
/score,/rerank,/pooling,/classifyのように/v1が付いていないパスは、--api-keyを指定しても認証されません。第8回で同様の性質のパスを実際に叩いてみました。
4. LoRAマルチサービング
ベースモデル1つにLoRAアダプターを複数載せ、リクエストごとに異なるアダプターを使用する機能です(--enable-lora --max-loras --max-lora-rank)。同じ8GB級GPUで1.7Bモデルにより測定した値はLoRAアダプターサービングにあります。アダプターを使用すると8.5〜17.9%遅くなり、アダプター数がスロット数を超えるとスループットが大幅に低下しました。今回の記事で追加する内容は2つあります。
- 実行中のアダプター読み込みは危険な機能です。
VLLM_ALLOW_RUNTIME_LORA_UPDATING=Trueで有効にする/v1/load_lora_adapterについて、文書は「隔離された完全に信頼できる環境ではない限り、本番環境で使用しないこと」と警告しています。リクエスト送信者がサーバーの読み取り先パスを指定できるようになるためです。 - 推論デコーディングと併用できません。 0.32.0の文書の機能互換表において、LoRAと推論デコーディングの交差部分は ❌ です(第3回)。
5. 検証できなかった事項
- TP・PP・DP・EPの実質的なメリットと通信コストは、GPUが1枚のため未計測です。
- 画像・音声入力、複数画像の処理、同時リクエストにおけるエンコーダーのレイテンシは未計測です。数値がずれた原因も未確認です。
- 埋め込みスループットのバッチサイズごとの変化、長文の再ランキング、精度評価は未計測です。
参考
- vLLM文書, Parallelism and Scaling, Data Parallel Deployment, Multimodal Inputs, Conserving Memory, Pooling Models, Security(0.32.0): https://docs.vllm.ai/en/latest/
- Shoeybi他, 「Megatron-LM: Training Multi-Billion Parameter Language Models Using Model Parallelism」, 2019. https://arxiv.org/abs/1909.08053
- vLLMサンプル, Qwen3再ランキングモデルを分類モデルとして立ち上げる: https://github.com/vllm-project/vllm/blob/v0.31.0/examples/pooling/score/qwen3_reranker_online.py
- vLLM 0.32.0 リリースノート(画像形式の許可リスト): https://github.com/vllm-project/vllm/releases/tag/v0.32.0
- 前回の記事: LoRAアダプターサービング, NVIDIA Dynamo