モジュール 10 · 第 5 回

vLLM でモデルサービスをデプロイする

多くのユーザーに同時にサービスを提供するとき、問題は同じ GPU でいかに多くのリクエストを処理するかに変わります。小さな GPT でまとめて処理する効果を測り、それから vLLM が KV キャッシュをどう管理し、OpenAI 互換のサービスをどう起動するかを説明します。

  • 約 35 分
  • 難易度:上級
  • 検証:2026-09-15 バッチ処理の実験は Apple M4 CPU で実行。vLLM の使い方は公式ドキュメントに従う

コードと実行結果は実際に動かしたときのまま載せているため、コメントと出力は中国語です。

前の課では Ollama で自分のパソコンでモデルを動かし、一人で使うにはとても便利でした。しかしモデルをサービスにして、社内の数百人や、あなたの製品のユーザーに使ってもらうとなると、問題は変わります。数十のリクエストが同時にやってきたとき、1~2 枚の GPU でどうやってできるだけ速く処理しきるのか。

vLLM は、現在最もよく使われているオープンソースの LLM サービス用フレームワークの一つです。この課ではまず実験でサーバー側の核心的な問題をはっきり見て、それから vLLM がそれをどう解決し、どう使うのかを扱います。

vLLM は主に NVIDIA の GPU を積んだ Linux サーバー向けで、私の実験用のマシン(Mac で、NVIDIA の GPU はない)では動きません。そのためこの課の実験はモジュール 09 の小さな GPT で行い、vLLM の使い方の部分は公式ドキュメントのコマンドだけを載せ、自分で実行して得たのではない出力は載せていません。

一首ずつ書くか、何首もまとめて書くか

モジュール 09 第 6 課で扱ったとおり、生成の各ステップではモデルのすべてのパラメータを一通り読むのに、計算されるのは一文字だけです。LLM では、このステップの時間は主に GPU メモリからパラメータを読むことに使われていて、計算ユニットはほとんどの時間データを待っています。

では、複数のリクエストを一緒にして、パラメータを一度読んで、複数のリクエストそれぞれに一文字ずつ同時に計算したらどうでしょうか。

for batch in [1, 4, 16, 64]:
    for _ in range(N_POEMS // batch):
        model.generate(torch.full((batch, 1), NEWLINE), N_TOKENS)  # batch 首诗同时写
python batch_throughput.py
一共要写 64 首,每首 60 个字,用 KV 缓存
  一次写  1 首:总共  1.84 秒,每秒   2085 个字,每一首从开始到写完 0.03 秒
  一次写  4 首:总共  0.93 秒,每秒   4110 个字,每一首从开始到写完 0.06 秒
  一次写 16 首:总共  0.48 秒,每秒   8053 个字,每一首从开始到写完 0.12 秒
  一次写 64 首:总共  0.44 秒,每秒   8706 个字,每一首从开始到写完 0.44 秒

同じく 64 首の詩を書かせると、一首ずつ書けば 1.84 秒、16 首まとめて書けば 0.48 秒で済み、1 秒あたりに書ける文字数(スループット)は約 4 倍になりました。

しかし最後の列に注目してください。1 首ずつ書くと、各詩は 0.03 秒で書き終わります。64 首まとめて書くと、どの詩も結果を受け取るまで 0.44 秒待たなければなりません。バッチが大きいほど全体の効率は上がりますが、各リクエストの待ち時間(レイテンシ)も長くなるのです。

しかも 16 から 64 では、スループットはほとんど上がっていません(8053 から 8706)。CPU の計算能力を使い切ってしまったのです。GPU の計算能力はずっと強いので、まとめて処理することから得られる利点もずっと大きくなります。

これこそモデルサービスが解決すべき核心の問題です。許容できるレイテンシの範囲で、できるだけ多くのリクエストをまとめて計算する

本物のサービスでの二つの難題

私たちの実験はとても理想的でした。64 首の詩が同時に始まり、どれも 60 文字書きます。本物のリクエストはそうではありません。

リクエストの長さはまちまちで、しかもいつでもやってくる。あるリクエストは 20 文字、別のリクエストは 2000 文字書く必要があります。それらを一つのバッチにして、最も長いものが書き終わるまで次のバッチを処理しないとすると、短いリクエストはとっくに書き終わっているのに場所を占め続け、新しく来たリクエストもただ待つしかありません。vLLM の方法は連続バッチ処理(continuous batching)です。一ステップ生成するたびに確認し、書き終わったリクエストはすぐにバッチから外し、新しく来たリクエストはすぐに加えます。バッチの中のリクエストは常に入れ替わり、GPU はずっと全力で働き続けます。

KV キャッシュは場所をとり、しかも大きさがわからない。モジュール 09 第 6 課でも前の課でも計算したとおり、KV キャッシュは文章が長くなるにつれて増え、各リクエストが最終的にどれだけ占めるかは前もってわかりません。従来のやり方は、最大の長さに合わせて各リクエストに連続した GPU メモリを丸ごと一塊確保するもので、ほとんどのリクエストはそんなに使わないので大量のメモリが無駄になり、同時に処理できるリクエストが少なくなります。

PagedAttention

vLLM の核となる発明は PagedAttention と呼ばれ、2023 年の論文『Efficient Memory Management for Large Language Model Serving with PagedAttention』に由来します。オペレーティングシステムがメモリを管理する「ページング」のやり方を借りています。

传统做法:每个请求预留一整块,按最大长度
  请求 A  [■■■■□□□□□□□□□□□□]   用了 4 格,空着 12 格
  请求 B  [■■■■■■□□□□□□□□□□]   用了 6 格,空着 10 格

分页:KV 缓存切成固定大小的小块,用到哪儿分配到哪儿
  块池    [A1][B1][A2][B2][B3][C1][空][空] ...
  请求 A 的块表:A1 → A2
  请求 B 的块表:B1 → B2 → B3

各リクエストの KV キャッシュは決まった大きさの小さなブロックに切り分けられ、連続して置く必要はなく、一つのブロックが埋まったら次のブロックを割り当て、各リクエストは実際に必要な分だけを使います。こうするとほとんど無駄がなく、同じ GPU メモリでずっと多くのリクエストに同時にサービスを提供できます。

複数のリクエストの冒頭が同じ(たとえば同じ system プロンプト)なら、これらのブロックを共有することもでき、それぞれに一つずつ保存する必要はありません。モジュール 06 第 4 課で扱った「キャッシュにヒットした入力は安い」は、サーバー側ではこれと似た仕組みです。

論文の要旨によれば、レイテンシが同じ条件で、vLLM のスループットは当時のほかのシステムの 2~4 倍で、文章が長く、モデルが大きいほど向上が目立ちました。

インストールと起動

以下のコマンドは vLLM の公式ドキュメントから引用したもので(2026 年 9 月時点)、NVIDIA の GPU を積んだ Linux マシンが必要です。vLLM は AMD や Intel の GPU、一部の CPU やその他のハードウェアにも対応していますが、インストール方法が違うので、公式ドキュメントを見てください。

uv venv --python 3.12 --seed
source .venv/bin/activate
uv pip install vllm --torch-backend=auto

サービスの起動はコマンド一つです。

vllm serve Qwen/Qwen2.5-0.5B-Instruct

既定では http://localhost:8000 で OpenAI 互換の API を提供します。前の課の Ollama と同じく、第 1 部のコードは環境変数を三つ変えるだけで使えます。

export LLM_BASE_URL=http://localhost:8000/v1
export LLM_API_KEY=你设置的密钥
export LLM_MODEL=Qwen/Qwen2.5-0.5B-Instruct

サービスを他の人に使ってもらうなら、必ずキーのチェックを加えてください。起動時に --api-key を付けるか、環境変数 VLLM_API_KEY を設定すれば、サーバーはリクエストヘッダーのキーをチェックします。モジュール 06 第 6 課で扱ったとおり、何の制限もない API をネットに置くのは、他人の代わりにお金を払っているのと同じです。

複数の LoRA を同時にぶら下げる

第 2、3 課で LoRA の利点を一つ述べました。ベースモデル一つに、たくさんの小さなアダプター。vLLM は一つのサービスで複数の LoRA を同時にぶら下げられ、リクエストごとにどれを使うかを指定できます。

vllm serve Qwen/Qwen2.5-0.5B-Instruct \
    --enable-lora \
    --lora-modules repobot=./.cache/repobot-lora

リクエストでモデル名を LoRA の名前 repobot にすれば、第 3 課でファインチューニングしたあのアダプターを使い、元のモデルの名前にすれば元のモデルを使います。複数の顧客がそれぞれ自分の LoRA を持っていても、占める GPU メモリはベースモデル一つ分だけです。

自分でデプロイするか、API を呼ぶか

ここまで学べば、モデルを自分でデプロイできます。しかし、そうすべきかどうかは別の問題です。

自分でデプロイするということは、GPU を買うか借りる必要があり、GPU が遊んでいるときもお金がかかり、拡張、監視、障害、アップグレードを自分で処理しなければならず、使えるモデルもあなたの GPU に入る大きさに限られる、ということです。

API を呼ぶなら、使った分だけ支払い、遊休のコストがなく、最も優れたモデルを使えます。モジュール 01 第 4 課で計算したとおり、DeepSeek のような API の料金はすでにとても安いのです。

一般に、自分でデプロイすることを検討する価値があるのは次のような場合です。データを自分のサーバーから出せない。呼び出し量が多く安定していて、自分でデプロイするほうが総コストが安い。自分でファインチューニングしたモデルを使う必要がある。決める前に、モジュール 06 第 4 課の方法で、二つの案のコストを実際に計算してみましょう。

練習問題

  1. batch_throughput.py にバッチ 8 と 32 を加え、あなたのパソコンでスループットがはっきりとは上がらなくなるバッチの大きさを見つけてください。
  2. 実験を変えて、一つのバッチの中の詩の長さをまちまちにし(たとえば半分は 20 文字、半分は 120 文字)、しかもバッチ全体が最も長いものを書き終えるまで終わらないようにしてください。すべて 60 文字書く場合と比べて、スループットはどれだけ下がりましたか。これこそ連続バッチ処理が解決する問題です。
  3. NVIDIA の GPU を積んだマシン(たとえばクラウドサーバー)が使えるなら、この課のコマンドで vLLM を起動し、モジュール 06 の RepoBot v4 からそれを呼び出してください。

確認テスト

1. 複数のリクエストをまとめて処理すると、スループットが上がるのはなぜですか?代償は何ですか?

生成の各ステップではモデルのパラメータを一通り読む必要があり、LLM では主な時間がパラメータを読むことに使われます。複数のリクエストをまとめて処理すれば、パラメータを一度読むだけで各リクエストに一文字ずつ計算でき、計算ユニットを十分に活用できます。代償は各リクエストのレイテンシが長くなることです。ほかのリクエストと一緒に計算しなければならず、しかもバッチがある程度大きくなると計算能力を使い切り、スループットも上がらなくなります。

2. 連続バッチ処理はどんな問題を解決していますか?

本物のリクエストは長さがまちまちで、いつでもやってきます。一つのバッチのリクエストが最も長いものを書き終えるまで次のバッチに替わらないとすると、短いリクエストは書き終えても場所を占め続け、新しいリクエストは待つしかありません。連続バッチ処理は各ステップで書き終わったリクエストを外し、新しいリクエストを加えて、GPU がずっと全力で働くようにします。

3. PagedAttention はどのように GPU メモリを節約していますか?

各リクエストの KV キャッシュを決まった大きさの小さなブロックに切り分け、必要に応じて割り当てるので、最大の長さに合わせて連続した GPU メモリを丸ごと確保する必要がなく、ほとんど無駄がありません。同じ前置きのブロックはリクエストの間で共有することもできます。節約した GPU メモリで、より多くのリクエストを同時に処理できるのです。

質問と議論

このレッスンでつまずいたところは、ここで質問してください。他の人の質問に答えるのも歓迎です。

質問で 3 ポイント、回答で 6 ポイント。審査を通過すると公開されます。

議論を読み込んでいます…