vast.ai で RTX 5090 を借りて ローカル LLM を動かす|2026-08 版
第 13 章

【実践】vLLM で OpenAI 互換 API を立てる

手元のコードから叩ける API サーバーにする。32 GB に収める設定が要点

この章で分かること

前章の Ollama は「1 コマンドで会話まで」の最短ルートでした。この章はその次の段、手元のエディタや自作スクリプトから叩ける API サーバーを立てます。使うのは vLLM です。設定する項目は増えますが、増えた分はほぼ全部「32 GB の VRAM をどう配分するか」に費やされます。そこさえ掴めば、あとは同じ形の繰り返しです。

お金の目安を先に置きます。この章の作業は合計 40 分ほどで、1 枚構成の RTX 5090 なら 0.27 USD ほど(2026-08-24 時点の実測中央値 0.4037 USD/時 で計算。1 USD = 150 円として約 41 円)。ここに、確保したディスクの代金(同時点の中央値 0.200 USD/GB/月)とモデルを落とすときの下り帯域(同 0.0026 USD/GB)が乗ります。

vLLM は「OpenAI 互換 API そのもの」

vLLM の実体は 2 つあります。Python ライブラリ(from vllm import LLM)と、それをラップした HTTP サーバ(vllm serve)です。この章で使うのは後者です。

ここが Ollama といちばん違うところです。Ollama は /api/chat という独自の API を本体に持ち、その横に OpenAI 互換の口を生やしていました。vLLM には独自 API 層がありません。/v1/completions や /v1/chat/completions が最初から本体です。だから「OpenAI の SDK を使っている既存のコードの、接続先だけを差し替える」という使い方が素直にできます。

観点Ollama(第 12 章)vLLM(この章)
API の形独自 API + OpenAI 互換の口OpenAI 互換 API が本体
既定のポート114348000
1 サーバが持つモデル数複数を同時に VRAM へ載せられる1 つだけ(公式: 「The server currently hosts one model at a time」)
モデルの取得元Ollama のライブラリ既定で Hugging Face Hub
モデルの指定ollama run gemma4vllm serve Qwen/Qwen3-8B(位置引数 = フラグ名を付けず並び順で渡す)
メモリの配分自動。溢れたら黙って CPU へ起動時に自分で決めて予約する

最後の行がこの章の主題です。Ollama は VRAM に収まらなければ勝手に CPU へ逃がして「動くけれど遅い」状態になりました。vLLM はそうしません。起動時に必要なメモリを計算し、足りなければその場でエラーを出して止まります。うるさいようですが、こちらのほうが原因が分かります。

前提条件も先に押さえます。vLLM が動く OS は Linux、Python は 3.10 から 3.13 です。Windows のネイティブ対応はありません(WSL かコミュニティ管理の fork を使うことになります)。macOS 向けには MLX をバックエンドにした vLLM-Metal がありますが、これは PyTorch ベースではない別物です。つまりこの章の話は、借りた Linux マシンの上でしか成立しません。手元の Mac に入れて試そうとしないでください。

32 GB は 3 つに割れる

RTX 5090 の VRAM は、vast.ai の offer が申告する値で 32607 MB(2026-08-24 時点、取得した 64 件すべて同一)でした。これは 2 進で数えると 約 31.84 GiB です。カタログの「32 GB」ではなく、この 31.84 GiB を分母にして計算します。

そして vLLM は、この 31.84 GiB を丸ごと使うわけではありません。--gpu-memory-utilization という引数が、GPU メモリのうち何割をモデル実行用に使うかを決めます。公式の説明はこうです。「The fraction of GPU memory to be used for the model executor, which can range from 0 to 1. ... If unspecified, will use the default value of 0.92.」既定は 0.92、つまり 92 % です。

RTX 5090 の VRAM 全体 = 31.84 GiB 触らない 8% vLLM がここを丸ごと予約する 31.84 GiB × 0.92 = 約 29.3 GiB モデルの重み 8B / bf16 なら 14.9 GiB 実行時 オーバーヘッド KV キャッシュ 残り全部がここ 量子化で縮む --gpu-memory-utilization がこの右端
図 13-1 横幅は 1 GiB = 約 20 px の実寸ですが、下段の右 2 つの比率は模式的です(実行時オーバーヘッドの実数は公式ドキュメントに記載がありません)。読み方の要点は順序です。まず右端が --gpu-memory-utilization で決まり、そこから重みが引かれ、KV キャッシュは最後に「残った分」を受け取るだけ。だから重みが太ると KV は自動的に痩せます。

必要 VRAM ≒ モデルの重み + KV キャッシュ + 実行時オーバーヘッド

3 項目を順に見ます。

① モデルの重みは事前に計算できます。vast.ai 公式の目安は「モデルパラメータ数 × 2 バイト(BF16 精度の場合)」。BF16 とは数値 1 つを 2 バイトで持つ形式で、量子化していない配布状態の代表的な精度です。8B のモデルなら 8 × 2 = 16 GB、2 進に直して 14.9 GiB です。量子化済みのモデルを選べばここが縮みます。静的に量子化されたモデル(Hugging Face 上の RedHatAI のリポジトリなど)は、公式によれば追加設定なしでそのまま読み込めます。

② 実行時オーバーヘッドは、CUDA コンテキストやアクティベーション、そして CUDA グラフ(= 毎回同じ順序で GPU に投げる一連の処理をひとまとまりに記録しておき、次回からは記録を再生して組み立て直しを省く仕組み)が占めます。vLLM は既定で CUDA グラフを使って推論を最適化しており、公式はこれが「追加の GPU メモリを食う」と明記しています。実数値は公式ドキュメントに書かれていませんので、ここは「読めない項」として扱ってください。どうしても削りたいときは --enforce-eager で CUDA グラフを無効化できます(起動は最速になりますが、定常時のデコード性能は落ちます)。

③ KV キャッシュ(= 会話の途中経過を GPU 上に置いておく作業領域。次節で詳しく見ます)は、①と②を引いた残り全部です。ここに何トークン分入るかが、次節のトレードオフの正体になります。実際にいくら確保されたかは起動ログに出ます。公式によれば、その値を次の起動で --kv-cache-memory-bytes に渡し直すと、メモリの実測(プロファイリング)と CUDA グラフのメモリ推定を丸ごと省いて起動を早められます。ただしこの値は同じ GPU・同じ初期空きメモリでしか通用しません。別のホストを借りて OOM したら、まずこのフラグを外して測り直します。

この「KV キャッシュ量を固定するフラグ」は公式ドキュメント内で綴りが 2 通りある

自動生成される Engine Arguments のリファレンスには --kv-cache-memory-bytes(説明は「Size of KV Cache per GPU in bytes」)と載っており、Optimization ページの本文は --kv-cache-memory と書いています。どちらが通るかは、借りたマシンで vllm serve --help を実行して確かめてください。綴りをドキュメントから写して動かない場合、まず疑うのはバージョン差です。

--gpu-memory-utilization は「1 インスタンスあたり」の上限

公式は「This is a per-instance limit, and only applies to the current vLLM instance. It does not matter if you have another vLLM instance running on the same GPU.」と書いています。つまり同じ GPU で vLLM を 2 つ動かしたいなら、両方に 0.5 を渡すという設計になります。逆に言うと、vLLM は他のプロセスが GPU をどれだけ使っているかを見てくれません。同じインスタンスで Ollama を立てたまま vLLM を起動すると、合計が VRAM を超えた時点で落ちます。

コンテキストを伸ばすと、同時に裁ける本数が減る

KV キャッシュには、1 リクエストが 1 トークン進むごとに決まった量が積み上がります。1 トークンあたりの量は、vast.ai の公式ドキュメントが式で示しています。

KV キャッシュ / トークン = 2 × 層数 × KV ヘッド数 × head_dim × 1 要素のバイト数

公式が挙げている計算例は 2 つです。Qwen3-30B-A3B(GQA = Grouped Query Attention。KV ヘッドを複数の Query ヘッドで共有して数を減らす構造)は 2 × 48 × 4 × 128 × 2 バイト で 約 96 KiB/token。GLM-4.7-Flash(MHA = Multi-Head Attention。共有せずヘッドの数だけ KV を持つ構造)は 2 × 47 × 20 × 256 × 2 バイト で 約 940 KiB/token。同じ「1 トークン」でも 約 10 倍違います。公式はこの差の原因を「KV ヘッドが 4 ではなく 20 あるため」と説明しています。パラメータ数が同じなら同じ VRAM で済む、という思い込みはここで崩れます。

単位はバイトで計算してから 1 回だけ変換する

公式ドキュメント自身の丸めが緩く、上の 2 つの例は KB(10 進)と KiB(2 進)の扱いが揃っていません。2 × 48 × 4 × 128 × 2 は正確には 98,304 バイトで、これは 96 KiB であり 98.3 KB です。まずバイトで通しで計算し、最後に一度だけ GiB へ直すのが安全です。

ここからが本題です。KV キャッシュの総量は起動時に固定されます。一方、1 本のリクエストが最大で何トークン分を占有するかは --max-model-len(プロンプトと出力を合わせたコンテキスト長の上限)が決めます。したがって、同時に何本裁けるかは割り算で決まります。

同時に処理できる本数 ≒ KV キャッシュの容量 ÷ (max-model-len × KV / トークン)

この割り算が出すのは「全部のリクエストが上限いっぱいまで伸びても詰まらない本数」、つまり最悪値です。実際の会話は上限まで使い切らないことのほうが多く、そのぶん同時に走れる本数は増えます。逆に、走らせている途中で KV が尽きたときに起きるのが後述の preemption です。見積もりはこの最悪値でやってください。楽観側で見積もると、混んだときだけ遅くなる不安定なサーバーになります。

KV キャッシュ 9 GiB ÷ 96 KiB/token = 98,304 トークン分(計算例) --max-model-len 8192 なら 12 本が同時に走る 12 本 --max-model-len 32768 なら 3 本 3 本 --max-model-len 131072 は 1 本も入らない 必要 131,072 トークン分 32,768 が溢れる 0 本 KV に入る上限 同時本数 = 98,304 ÷ max-model-len(切り捨て)
図 13-2 箱 1 つが「1 本のリクエストが最大で占有する KV」です。縦の線が動かないことがこの図の主張で、伸ばせるのは 1 本あたりの長さだけ、増やせるのは本数だけ。一番下は 1 本すら入らない状態で、この場合 vLLM は起動時にエラーを出して止まります(受け付けてから遅くなるのではありません)。

実際にそのエラーを踏んだ例が vast.ai の公式ドキュメントに載っています。MiniMax-M2 を 4 枚構成で立てようとしたときのものです。

ValueError: To serve at least one request with max seq len (196608), 11.62 GiB KV cache is needed

公式の解決策は「フルの 196K コンテキストは 4×80GB GPU に収まらない。--max-model-len 131072 を使う」でした。読者がこの章で最初に踏むのもこのエラーです。犯人はほぼ常に --max-model-len です。

もう 1 つ、KV が足りないときに起きるのが preemption です。こちらは起動には成功していて、走らせている最中に性能が落ちます。vLLM は KV が枯渇するとリクエストを一旦追い出し、あとで計算し直します。そのときログにこう出ます。

WARNING 05-09 00:49:33 scheduler.py:1057 Sequence group 0 is preempted by PreemptionMode.RECOMPUTE mode because there is not enough KV cache space. This can affect the end-to-end performance. Increase gpu_memory_utilization or tensor_parallel_size to provide more KV cache memory. total_cumulative_preemption_cnt=1

公式が挙げる対処は 4 つで、優先順に ① gpu_memory_utilization を上げる ② max_num_seqs または max_num_batched_tokens を下げる ③ tensor_parallel_size を上げる ④ pipeline_parallel_size を上げるです。1 枚しか借りていないなら③と④は使えないので、実質は①と②の 2 択になります。

引数何を決めるか上げると下げると
--gpu-memory-utilizationVRAM のうち vLLM が予約する割合(既定 0.92)KV が増える他プロセスと同居できる
--max-model-len1 本あたりのコンテキスト長の上限。未指定ならモデルの設定から自動導出長い文脈を扱える/同時本数が減る同時本数が増える
--max-num-seqs1 回のイテレーションで処理する最大シーケンス数(=同時本数の上限)スループットが伸びる必要な KV が減る
--max-num-batched-tokenschunked prefill(= 長いプロンプトの下処理を小分けにして、生成中のリクエストと同じバッチに混ぜる方式)のトークン予算TTFT(初回トークンまで)が良くなるITL(トークン間)が良くなる
--enforce-eagerCUDA グラフを使わない起動が最速・メモリを節約/定常デコード性能は落ちる
--served-model-nameAPI 上のモデル名。未指定なら渡したモデル名がそのまま名前になる手元のコードに書く model の値を短くできる

スループットを最優先するなら、公式は max_num_batched_tokens を 8192 より大きくすることを勧めています(特に大きい GPU で小さいモデルを動かす場合)。逆に応答が途切れがちな体感を嫌うなら 2048 程度に下げます。なお vLLM V1 では chunked prefill は可能な限り既定で有効なので、この値を意識するのはチューニング段階で構いません。

vast.ai のテンプレートで立てる

立て方は 2 通りあります。① vast.ai の vLLM テンプレートを使う道と、② 素のインスタンスに自分で入れる道です。この章は①を通します。vllm serve の起動をテンプレートがやってくれるので、読者が触るのは環境変数 2 本だけで済むからです。

環境変数役割テンプレートの既定値
VLLM_MODELサーブするモデル(Hugging Face のリポジトリ名)deepseek-ai/DeepSeek-R1-Distill-Llama-8B
VLLM_ARGSvllm serve に渡す引数をそのまま並べるテンプレート既定

既定モデルが選ばれている理由も公式に書かれています。「it can run on a single GPU with only 21GB VRAM」(コンテキストウィンドウは制限付き)。31.84 GiB の RTX 5090 なら余裕があります。ディスクはモデルサイズ以上を確保します。公式はこのモデルについて 17 GB 超をスライダーで割り当てるよう書いています。本書ではここに、あとで別のモデルを試したときの置き場と torch.compile のキャッシュ分を上乗せして 40 GB を例に使います(この 40 という数字は本書の判断で、公式の推奨値ではありません)。ディスクのサイズはインスタンスを作ったあとは変更できません。

ディスクの月額 = 確保 GB × storage_cost(USD/GB/月)

計算例: 40 GB を 1 か月確保すると 40 × 0.200 = 月 8 USD(2026-08-24 時点の中央値で計算)。GPU を 1 時間動かすのが 0.40 USD ですから、使い終わったインスタンスを放置するほうが高くつきます。

Docker イメージは絶対に差し替えない

RTX 5090(Blackwell 世代)は旧世代ほどの後方互換性を持ちません。vast.ai 公式は「テンプレートと Docker イメージが CUDA 12.8 および PyTorch 2.7 以上を使うように構成されていることが重要」と明記し、レンタル手順の中で「Docker イメージを変更してはいけない」と警告しています。[Automatic] タグのテンプレートは対応済みの最新イメージを自動で引きます。イメージ名の欄には触らないでください。

やってみる 13-1|テンプレートを設定して API を立てる

テンプレートの編集からインスタンス起動、そして /v1/models が答えるところまで。所要 20 分(大半はモデルのダウンロード待ち)、課金は約 0.13 USD(2026-08-24 時点の実測中央値 0.4037 USD/時 で計算)。

  1. cloud.vast.ai/templates で推奨の vLLM テンプレートを開き、鉛筆ボタンを押してテンプレートエディタに入る。
  2. VLLM_MODEL と VLLM_ARGS を確認する。まずは既定のまま保存して構わない。保存したものは My Templates セクションに入る。
  3. play アイコンを押して検索画面に移り、ディスクスライダーを 40 GB に合わせる(スライダーはテンプレートのアイコンの下、左の列にある)。ここは作成後に変更できない。
  4. 1× RTX 5090 の offer を選んで Rent を押す。この瞬間から時間課金が始まる。
  5. インスタンスカードのログボタンでモデルのダウンロード進捗を見る。API がすぐ応答しないのは正常。
  6. SSH で入って、インスタンスの中から自分自身の 8000 番に問い合わせる。
curl http://localhost:8000/v1/models

サーブ中のモデル ID を含む JSON が返れば成功です。Connection refused が返るうちは、まだ起動シーケンスの途中です。認証エラー(401 など)が返ったときは、テンプレート側で API キーが設定されています。その場合は -H "Authorization: Bearer <OPEN_BUTTON_TOKEN>" を足して叩き直してください(トークンはインスタンスカードの Open ボタンや Instance Portal のリンクから読めます)。起動が終わると、インスタンスカードの Open ボタンから開く Instance Portal に、対話的な vLLM API ドキュメントと Ray コントロールパネルへのリンクが並びます。

初回だけ通る 4 段階(2 回目以降はモデル DL が消える) この帯の全部が GPU 時間課金(0.4037 USD/時・2026-08-24 中央値) Rent を押す 課金開始 イメージを pull instance logs モデルの DL 初回だけ・長い 重みをロード KV 領域を計測 /v1/models が応答する この間 curl は繋がらない(= 壊れているわけではない) ここで 200 経過時間(縮尺は実際の比率ではない) 17 GB を 752 Mbps で落として約 3 分。HF 側の制限で 1 時間かかることもある
図 13-3 箱の幅は所要時間ではありません(順序だけを示しています)。下段の数字は 2026-08-24 時点の実測下り速度の中央値 752 Mbps から出した本書の計算例で、vast.ai 公式は別の場所で「HuggingFace がダウンロード速度を制限しているため、非常に速いインスタンスでも最大 1 時間かかることがある」と書いています。待っている時間もそのまま課金されます。

1 回で狙いどおりに立つことは稀です。落ちたら VLLM_ARGS を直して立て直す、を繰り返します。次はその 1 周目です。

やってみる 13-2|32 GB に収める引数を入れて立て直す

テンプレートエディタに戻り、VLLM_ARGS を明示します。所要 10 分、課金は約 0.07 USD。モデルは 2 回目以降ディスクに残っているので、ダウンロードは繰り返されません。

  1. テンプレートエディタで VLLM_ARGS に次の 3 つを並べる。
  2. 保存して起動し直し、ログで KV キャッシュに何 GiB 割り当てられたかを読む。
  3. その値と、使うモデルの KV / トークンから、同時に何本裁けるかを検算する。
--max-model-len 32768 --gpu-memory-utilization 0.90 --max-num-seqs 8

--max-model-len 32768 は「1 本あたり最大 32,768 トークン」。--gpu-memory-utilization 0.90 は既定の 0.92 よりわずかに控えめで、同じ GPU に他のプロセスが少し居ても落ちにくくなります。--max-num-seqs 8 は同時本数の上限を 8 本に切る指定です。ここで落ちるなら、下げるのはまず --max-model-len です。

テンプレートを使わない道も 1 度は見ておく価値があります。素の CUDA 環境に自分で入れる場合、公式が第一手に挙げているのは uv で、uv pip install vllm --torch-backend=auto の --torch-backend=auto が肝です。これはインストール済みの CUDA ドライバのバージョンを見て、適切な PyTorch のインデックスを自動で選びます。Blackwell 世代のドライバ要件を自分で調べなくて済みます。Docker なら公式イメージは vllm/vllm-openai:latest で、モデル名はイメージ名の後ろに位置引数として渡します。

docker run --rm --gpus all \
    -v ~/.cache/huggingface:/root/.cache/huggingface \
    -v vllm-cache:/root/.cache/vllm \
    -p 8000:8000 \
    vllm/vllm-openai:latest \
    Qwen/Qwen3-8B

2 つのボリュームには別々の役割があります。~/.cache/huggingface はモデルの重みを残すため。~/.cache/vllm は torch.compile の成果物を残すためで、これを付けないとコンテナを作り直すたびに再コンパイルが走ります。なお gated なモデル(Llama 系など)は Hugging Face 上でアクセス申請して承認を得たうえで、HF_TOKEN を渡す必要があります。

手元のコードから叩く

ここまでで API は立っています。あとは手元のコードの向き先を変えるだけです。OpenAI の Python クライアントを使う場合、変えるのは base_url と api_key の 2 つだけです。

base_url が経路を決め、api_key は vLLM の中で照合される 手元の Mac のコード base_url = .../v1 api_key = "..." model = HF のリポジトリ名 SSH トンネル 平文の 8000 番に直結 公開ポート HTTPS + 自己署名 /v1/chat/completions --api-key で守られる /invocations 同じ推論が素通り 経路 ① 経路 ② 素で届く -k が要る ここで OPEN_BUTTON_TOKEN も要る --api-key は /v1 系だけを守る。/invocations は同じ推論をそのまま返す
図 13-4 左の 3 行のうち経路を決めるのは base_url だけで、api_key は経路に関係なく vLLM の中まで運ばれてから照合されます。右下がこの章でいちばん危険な箱で、同じ HTTP サーバの上にありながら --api-key の守備範囲の外にあります(詳細はこの章のつまずきポイント)。

3 つの値をどう埋めるかを順に決めます。

経路base_url に書く値認証証明書
① SSH トンネル(推奨)http://localhost:8000/v1vLLM の --api-key だけ不要(平文が SSH の中を通る)
② 公開ポートhttps://<IP>:<外部ポート>/v1vast.ai の OPEN_BUTTON_TOKEN を Bearer で自己署名。curl なら -k が要る

初心者に勧めるのは経路 ① です。SSH トンネルとは、SSH の接続の中に別の通信を通して、手元の localhost:8000 を借りたマシンの 8000 番にそのまま繋げる仕組みのことです。理由は 2 つあります。1 つは第 9 章で作った ssh -L の型がそのまま使えること。もう 1 つは、vast.ai が自己署名証明書を使うため、証明書を検証する普通のクライアント(Python の SDK を含む)は経路 ② をそのままでは通れないことです。curl -k に相当する回避をライブラリごとに調べるより、トンネルを 1 本張るほうが早く済みます。

やってみる 13-3|OpenAI の SDK で借りた GPU を呼ぶ

手元の Mac / PC から、いま立てた vLLM に接続します。所要 10 分、課金は約 0.07 USD。インスタンスは起動したままにしておきます。

  1. 手元のターミナルでトンネル付きで接続する。<ポート> と <ホスト> はインスタンスカードの SSH ボタンから写します。そのウィンドウは繋いだまま放置します。
  2. 手元の別のターミナルで、まず curl でモデル ID を確かめる。ここで返ってきた id が、次のコードの model に書く値です。
  3. OpenAI の Python クライアントを入れて(pip install openai)、接続先だけ差し替えたコードを走らせる。
ssh -p <ポート> root@<ホスト> -L 8000:localhost:8000
curl http://localhost:8000/v1/models
from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8000/v1",
    api_key="EMPTY",
)

completion = client.chat.completions.create(
    model="deepseek-ai/DeepSeek-R1-Distill-Llama-8B",
    messages=[{"role": "user", "content": "自己紹介を 1 文で"}],
)
print(completion.choices[0].message)

応答が表示されれば、手元のコードから借りた GPU を呼べている状態です。OpenAI にしかないパラメータではなく vLLM 独自の値(top_k など)を渡したいときは、extra_body={"top_k": 50} のように extra_body に入れます。

経路 ② を使うのは「テンプレートで借りて、そのまま外から叩きたい」場合です。URL は https://INSTANCE_IP:PORT_8000 の形で、Authorization ヘッダに OPEN_BUTTON_TOKEN の値を Bearer トークンとして入れます。外部ポート番号はインスタンスカードの IP ボタンから内部 8000 に対応するものを読みます。

curl -k https://<インスタンスの IP>:<外部ポート>/v1/completions \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer <OPEN_BUTTON_TOKEN>" \
    -d '{
       "model": "deepseek-ai/DeepSeek-R1-Distill-Llama-8B",
       "prompt": "San Francisco is a",
       "max_tokens": 128,
       "temperature": 0.6
     }'

つまずきポイント

「起動しない」と思ったら、モデルを落としている最中だった

vast.ai 公式も「初回起動時はモデルのダウンロードのため API がすぐには使えない」と明記しています。curl が Connection refused を返す間は、まずインスタンスカードのログボタンで進捗を見てください。壊れているかどうかの判断はそのあとです。公式が挙げている別の大型モデルの例では、初回デプロイに合計 30 分ほどかかり、「45 分以上止まっていたらログのエラーを確認」という目安が示されています。待っている時間も 1 秒単位で課金されているので、ダウンロードが長引きそうなモデルは借りる前にサイズを調べておきます。

--api-key を付けたから公開しても安全だと思った

安全ではありません。公式が明示的に警告しています。--api-key(および VLLM_API_KEY)が認証するのは /v1・/v2・/inference の接頭辞を持つエンドポイントだけで、/invocations は認証されないまま同じ推論機能を晒します。原文は「Do not rely on --api-key alone to secure vLLM.」。対策として公式が挙げているのはリバースプロキシ(= 外からの通信をいったん受け止め、認証してから後ろのサーバへ中継する別のサーバ)配下への配置です。この本の読者に勧められるのはそもそも外に開けないこと、つまり SSH トンネル経由で使うことです。

GPU 使用率が伸びない。犯人は CPU のことがある

vLLM は N 個の GPU に対して「API サーバ 1 + エンジンコア 1 + GPU ワーカー N」の合計 2 + N プロセスが常に CPU を取り合います。公式は必要な物理コア数の最小値を 2 + N、ハイパースレッディング有効なら最低 2 × (2 + N) vCPU としており、「エンジンコアのプロセスはビジーループで動くので CPU 飢餓に特に弱い」と書いています。1 枚借りるなら物理 3 コア / 6 vCPU が下限です。vast.ai では 8 枚挿しマシンの 1 枚だけを借りると CPU も 8 分の 1 しか付いてきます(offer の gpu_frac が 0.125 のような値になっているのがその状態)。GPU が遊んでいたら、offer の CPU 割り当てを疑ってください。

Python から直接使ったら出力がおかしい

vllm serve ではなく Python ライブラリとして使うときの罠です。公式は「The llm.generate method does not automatically apply the model's chat template to the input prompt.」と明記しています。Instruct / Chat モデルに llm.generate を使うと、チャットテンプレートが適用されないまま素のテキスト補完になります。メッセージ配列を渡す llm.chat を使ってください。この章で立てた HTTP サーバの /v1/chat/completions は、テンプレートを適用する側の口なのでこの問題は起きません。

理解度チェック

Q1. --gpu-memory-utilization の既定値はいくつで、その値は何に対する割合ですか?

0.92 です。GPU メモリのうちモデル実行用(model executor)に使う割合を 0 から 1 で指定します。RTX 5090 の実効 VRAM 31.84 GiB なら約 29.3 GiB を予約する計算です。これは 1 つの vLLM インスタンスに対する上限で、同じ GPU で他のプロセスが何を使っているかは考慮されません。

Q2. KV キャッシュに 9 GiB 割り当てられ、モデルの KV が 96 KiB/token でした。--max-model-len を 32768 にすると同時に何本裁けますか?

3 本です。まずバイトで通します。9 GiB = 9,663,676,416 バイト、96 KiB = 98,304 バイトなので、合計 98,304 トークン分の KV があります。これを 1 本あたり 32,768 トークンで割ると 3 本。--max-model-len を 8192 に下げれば 12 本になり、131072 に上げると1 本も入らず起動時にエラーで止まります。

Q3. パラメータ数が同じ 2 つのモデルで、KV キャッシュの必要量が 10 倍違うことがあります。何が原因ですか?

アテンションの構造(KV ヘッドの数)です。KV / トークンの式は 2 × 層数 × KV ヘッド数 × head_dim × 要素のバイト数 なので、KV ヘッドが多いほど比例して重くなります。vast.ai 公式の 2 例では、GQA の Qwen3-30B-A3B が約 96 KiB/token(KV ヘッド 4)、MHA の GLM-4.7-Flash が約 940 KiB/token(KV ヘッド 20)でした。「パラメータ数が同じなら同じ VRAM」は成り立ちません。

Q4. --api-key を設定した vLLM を、グローバル IP で直接公開してもよいですか?

いけません。--api-key が認証するのは /v1・/v2・/inference 接頭辞のエンドポイントだけで、/invocations は同じ推論機能を認証なしで晒します。公式も「Do not rely on --api-key alone to secure vLLM.」と書き、リバースプロキシ配下に置くよう案内しています。手元から使うだけなら ssh -L 8000:localhost:8000 でトンネルを張り、外にポートを開けないのが最も安全です。

Q5. OpenAI の Python クライアントで vLLM に繋ぐとき、base_url と api_key にはそれぞれ何を書きますか?

base_url はサーバのアドレスに /v1 まで含めた値(トンネル経由なら http://localhost:8000/v1)。api_key は --api-key に渡した文字列で、渡していないサーバには "EMPTY" と書きます(省略はできません)。model にはサーブ中のモデル ID、つまり Hugging Face のリポジトリ名をそのまま入れます。実際の値は curl http://localhost:8000/v1/models で確認できます。

まとめ

次章では同じ 32 GB の制約を、llama.cpp と GGUF という別の道具で攻めます。vLLM が「bf16 の重みをそのまま載せて配分を調整する」道具だったのに対し、あちらは「重みを最初から小さくして載せる」道具です。Q4_K_M のような量子化タイプの読み方から始めます。