LLM高速推論ライブラリ「vLLM」を試す
ちょっとしか触ったことがなかっただけど、以下を見て一通り試しておこうかと。
GitHubレポジトリ
概要
vLLMは、LLMの推論およびサービングのための、高速で使いやすいライブラリです。
vLLMは、もともとUC BerkeleyのSky Computing Labで開発され、その後、学術機関と産業界の貢献によって、コミュニティ主導のプロジェクトへと発展しました。
vLLMは以下の点で高速です:
- 最先端のサービングスループット
- PagedAttentionを活用した効率的なアテンションキー・バリューメモリ管理
- 連続的なバッチ処理
- CUDA/HIPグラフによる高速なモデル実行
- 量子化技術のサポート:GPTQ、AWQ、INT4、INT8、FP8
- FlashAttentionおよびFlashInferと統合された最適化CUDAカーネル
- 投機的デコーディング(Speculative Decoding)
- チャンク化されたプリフィル
パフォーマンスベンチマーク:
こちらのブログ記事の最後に、vLLMのパフォーマンスベンチマークを掲載しています。このベンチマークでは、vLLMと他のLLMサービングエンジン(TensorRT-LLM, SGLang, LMDeploy)の比較を行っています。
実装はnightly-benchmarksフォルダにあり、こちらのワンクリックスクリプトを使って再現できます。
vLLMは、柔軟で使いやすい設計になっています:
- Hugging Faceの人気モデルとシームレスに統合
- 並列サンプリング、ビームサーチなどの多様なデコーディングアルゴリズムによる高スループットサービング
- 分散推論のためのテンソル並列処理およびパイプライン並列処理のサポート
- ストリーミング出力対応
- OpenAI互換のAPIサーバー
- NVIDIA GPU、AMD CPU/GPU、Intel CPU/GPU、PowerPC CPU、TPU、AWS Neuronのサポート
- プレフィックスキャッシングのサポート
- Multi-LoRAのサポート
vLLMは、Hugging Face上の主要なオープンソースモデルを幅広くサポートしています:
- Transformer系LLM(例:Llama)
- Mixture-of-Expert LLM(例:Mixtral、Deepseek-V2、Deepseek-V3)
- 埋め込みモデル(例:E5-Mistral)
- マルチモーダルLLM(例:LLaVA)
対応モデルの全リストはこちらで確認できます。
公式ドキュメントはこちら
インストール
以下の環境で試す
- Ubuntu-22.04
- RTX4090(24GB)
- Python-3.12.9
- CUDA-12.6
ということでGPUの場合の手順を参考に。
作業ディレクトリ作成
mkdir vllm-work && cd vllm-work
Python仮想環境作成。最近はuvを使っている。
uv venv --python 3.12
パッケージインストール
uv pip install vllm
Installed 133 packages in 1.05s
(snip)
+ vllm==0.7.1
(snip)
Quickstart
vLLMを使った推論は以下の2つの方法がある
- オフラインバッチ推論
- OpenAI互換サーバを使用したオンライン推論
オフラインバッチ推論
サンプルコードが以下に用意されている。
以下のモデルを使用した。
from vllm import LLM, SamplingParams
# サンプルプロンプト
prompts = [
"こんにちは!私の名前は、",
"日本の総理大臣は、",
"日本の首都は、",
"AIの未来は、",
]
# サンプリングパラメータを定義
sampling_params = SamplingParams(temperature=0.8, top_p=0.95)
# LLMを定義
llm = LLM(model="google/gemma-2-2b-it")
# プロンプトからテキストを生成します。出力は、プロンプト、生成されたテキスト、
# その他の情報を含むRequestOutputオブジェクトのリスト。
outputs = llm.generate(prompts, sampling_params)
# 出力
for output in outputs:
prompt = output.prompt
generated_text = output.outputs[0].text
print(f"プロンプト: {prompt!r}, 生成されたテキスト: {generated_text!r}")
実行
uv run offline_inference.py
結果
プロンプト: 'こんにちは!私の名前は、', 生成されたテキスト: ' 〇〇です。\n今日は、〇〇についてお話します。\n\nこの文は'
プロンプト: '日本の総理大臣は、', 生成されたテキスト: '誰が就任しましたか?\n\n**答え:** Fumio Kishida\n\n**'
プロンプト: '日本の首都は、', 生成されたテキスト: '\na) 上海\nb) 東京\nc) 北京\nd) 香港'
プロンプト: 'AIの未来は、', 生成されたテキスト: '私たちの想像を超える可能性を秘めている。その可能性を実現するために、私たちはAI'
基本は以下のような流れみたい
-
LLM
クラスでLLMを定義 -
SamplingParams
でサンプリングパラメータを定義 -
LLM.generate
にプロンプト、サンプリングパラメータを渡して、生成 - 結果が
RequestOutput
のリストで返される
OpenAI互換サーバを使用したオンライン推論
vLLMはOpenAI互換APIサーバも提供している。vllm
コマンドを使う。
Usageをまず見てみる。
uv run vllm --help
usage: vllm [-h] [-v] {serve,complete,chat} ...
vLLM CLI
positional arguments:
{serve,complete,chat}
serve Start the vLLM OpenAI Compatible API server
complete Generate text completions based on the given prompt via the running API server
chat Generate chat completions via the running API server
options:
-h, --help show this help message and exit
-v, --version show program's version number and exit
serve
サブコマンドでOpenAI互換APIサーバを起動することができる。余談だが、コマンドラインクライアント的にも使えるように見える。
serve
のUsageもみてみるが、めちゃめちゃ長いので一部だけ。
usage: vllm serve <model_tag> [options]
positional arguments:
model_tag The model tag to serve
options:
(snip)
--host HOST Host name.
(snip)
--port PORT Port number.
(snip)
デフォルトではhttp://localhost:8000
で待ち受けるようなので、必要ならばオプションを指定する。
では起動。自分はLAN内のリモートサーバなので--host
・--port
オプションを指定した。
uv run vllm serve google/gemma-2-2b-it \
--host 0.0.0.0 \
--port 8888
起動した。以下のようなエンドポイントがある様子。
(snip)
INFO 02-06 22:58:49 launcher.py:19] Available routes are:
INFO 02-06 22:58:49 launcher.py:27] Route: /openapi.json, Methods: GET, HEAD
INFO 02-06 22:58:49 launcher.py:27] Route: /docs, Methods: GET, HEAD
INFO 02-06 22:58:49 launcher.py:27] Route: /docs/oauth2-redirect, Methods: GET, HEAD
INFO 02-06 22:58:49 launcher.py:27] Route: /redoc, Methods: GET, HEAD
INFO 02-06 22:58:49 launcher.py:27] Route: /health, Methods: GET
INFO 02-06 22:58:49 launcher.py:27] Route: /ping, Methods: GET, POST
INFO 02-06 22:58:49 launcher.py:27] Route: /tokenize, Methods: POST
INFO 02-06 22:58:49 launcher.py:27] Route: /detokenize, Methods: POST
INFO 02-06 22:58:49 launcher.py:27] Route: /v1/models, Methods: GET
INFO 02-06 22:58:49 launcher.py:27] Route: /version, Methods: GET
INFO 02-06 22:58:49 launcher.py:27] Route: /v1/chat/completions, Methods: POST
INFO 02-06 22:58:49 launcher.py:27] Route: /v1/completions, Methods: POST
INFO 02-06 22:58:49 launcher.py:27] Route: /v1/embeddings, Methods: POST
INFO 02-06 22:58:49 launcher.py:27] Route: /pooling, Methods: POST
INFO 02-06 22:58:49 launcher.py:27] Route: /score, Methods: POST
INFO 02-06 22:58:49 launcher.py:27] Route: /v1/score, Methods: POST
INFO 02-06 22:58:49 launcher.py:27] Route: /rerank, Methods: POST
INFO 02-06 22:58:49 launcher.py:27] Route: /v1/rerank, Methods: POST
INFO 02-06 22:58:49 launcher.py:27] Route: /v2/rerank, Methods: POST
INFO 02-06 22:58:49 launcher.py:27] Route: /invocations, Methods: POST
INFO: Started server process [1937112]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8888 (Press CTRL+C to quit)
では別のマシンからcurlで叩いてみる。
curl http://[サーバのIP]:8888/v1/completions \
-H "Content-Type: application/json" \
-d '{
"model": "google/gemma-2-2b-it",
"prompt": "競馬の魅力を5つリストアップするならば、",
"max_tokens": 1024,
"temperature": 0
}' | jq -r .
{
"id": "cmpl-7dd42f61d4f74b76b2397e7b60ce0287",
"object": "text_completion",
"created": 1738850462,
"model": "google/gemma-2-2b-it",
"choices": [
{
"index": 0,
"text": "\n\n1. **競争の激しさ**: 馬と騎手の連携、そしてレース展開の予測性。\n2. **予想の面白さ**: 馬の能力、騎手の戦略、そして運の要素。\n3. **歴史と伝統**: 長い歴史の中で培われた文化、そして伝統的なレース。\n4. **人とのつながり**: 競馬場での交流、そしてレースを通して生まれる友情。\n5. **興奮と感動**: レースの展開、そして勝利の喜び。\n\nこれらの要素が組み合わさって、競馬の魅力を最大限に引き出す。\n\n**補足**: \n\n* 競馬は、単に馬と競争するスポーツではなく、戦略、分析、そして運の要素が複雑に絡み合っています。\n* 競馬は、人々の心を揺さぶるスポーツであり、その魅力は人それぞれに感じられるでしょう。 \n\n\n",
"logprobs": null,
"finish_reason": "stop",
"stop_reason": 107,
"prompt_logprobs": null
}
],
"usage": {
"prompt_tokens": 11,
"total_tokens": 203,
"completion_tokens": 192,
"prompt_tokens_details": null
}
}
OpenAI SDKでも試してみる。こちらはChat Completions APIで。
from openai import OpenAI
client = OpenAI(
api_key="dummy",
base_url="http://[サーバのIP]:8888/v1"
)
stream = client.chat.completions.create(
model="google/gemma-2-2b-it",
messages=[
{
"role": "user",
"content": "競馬の魅力を5つリストアップして。",
}
],
stream=True,
)
for chunk in stream:
print(chunk.choices[0].delta.content or "", end="")
競馬の魅力を5つリストアップします。
1. **予測不可能で、スリリングな展開:** 競馬はまさに「誰が勝つのか」という自然の物語です。予想通りの展開ではなく、時には予想外のレース展開が魅力的で興奮を誘います。
2. **賭け方次第で、経済的な利益を得る可能性:** 競馬は賭博の一種です。しかし、知識・分析、そして戦略的な賭け方次第で、成功の可能性もあります。
3. **歴史と伝統:** 競馬は長い歴史と伝統を誇り、その伝統的な格式感はただレースだけを楽しむだけでなく、それを楽しむ文化を形作っています。
4. **憧憬と夢:** 競馬では、「夢を叶える」という願望や「成功と栄光」を追求する人々が集い、その熱狂的な雰囲気は競馬場の雰囲気を盛り上げる大きな要素です。
5. **静かな夜と、興奮の瞬間:** 競馬場では、静かな夜を過ごす人もいれば、興奮した瞬間を味わう人もいます。競馬場には、様々な人の集いがあり、それぞれの魅力が楽しめます。
競馬は、単なるスポーツだけではなく、歴史、文化、経済、そして人の夢や感情と深く結びついています。
その全てが、競馬の魅力を生み出しています。
なお、1サーバでロードできるのは1モデルだけみたい。
量子化
vLLMは量子化もサポートしている。ドキュメントに記載があるのは以下。
- AutoAWQ
- BitsAndBytes
- GGUF
- INT4 W4A16
- INT8 W8A8
- FP8 W8A8
- Quantized KV Cache
BitsAndBytesを使って4ビット量子化してみる。
パッケージインストール
uv pip install bitsandbytes>=0.45.0
Installed 1 package in 107ms
+ bitsandbytes==0.45.1
上のオフライン推論のスクリプトを修正してみる。
from vllm import LLM, SamplingParams
import torch
(snip)
llm = LLM(
model="google/gemma-2-2b-it",
dtype=torch.bfloat16,
quantization="bitsandbytes",
load_format="bitsandbytes",
)
(snip)
実行してみると以下のように出力される。
(snip)
INFO 02-06 23:21:34 loader.py:1078] Loading weights with BitsAndBytes quantization. May take a while ...
(snip)
INFO 02-06 23:21:37 worker.py:266] model weights take 2.09GiB; non_torch_memory takes 0.08GiB; PyTorch activation peak memory takes 2.36GiB; the rest of the memory reserved for KV Cache is 16.63GiB.
(snip)
ちなみに量子化なしの場合は以下のように表示されていたので、一応効いていると考えてもいいのかな。
INFO 02-06 22:42:27 worker.py:266] model weights take 4.90GiB; non_torch_memory takes 0.08GiB; PyTorch activation peak memory takes 2.36GiB; the rest of the memory reserved for KV Cache is 13.82GiB.
OpenAI互換APIサーバの場合。以下のようにオプションを追加する。
uv run vllm serve google/gemma-2-2b-it \
--host 0.0.0.0 \
--port 8888 \
--quantization bitsandbytes \
--load-format bitsandbytes
量子化してもVRAMはあるだけ使う(KVキャッシュのため)ってことなのかな?オプションあり/なし、それぞれのnvidia-smiを見ても違いはほとんどないように見える。
Thu Feb 6 23:32:17 2025
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 560.35.03 Driver Version: 560.35.03 CUDA Version: 12.6 |
|-----------------------------------------+------------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+========================+======================|
| 0 NVIDIA GeForce RTX 4090 Off | 00000000:01:00.0 Off | Off |
| 0% 46C P8 17W / 450W | 20495MiB / 24564MiB | 0% Default |
| | | N/A |
+-----------------------------------------+------------------------+----------------------+
+-----------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=========================================================================================|
| 0 N/A N/A 1644271 G /usr/lib/xorg/Xorg 167MiB |
| 0 N/A N/A 1644372 G /usr/bin/gnome-shell 15MiB |
| 0 N/A N/A 1942547 C ...ository/vllm-work/.venv/bin/python3 20276MiB |
+-----------------------------------------------------------------------------------------+
まとめ
とりあえずサラッと触りだけやってみた。高速なのかどうか?はこれぐらいじゃわからないけれども、以下を見る限りは高速みたい。
ドキュメントはめちゃめちゃ豊富、というかめちゃめちゃ多いので、一通り機能的なところをカバーしてみよう、みたいなのは諦めた。具体的にこういうことがやりたい、とか、こういうときはどうすれば?みたいな感じなら、だいたい書いてありそうな雰囲気を感じた。
というわけでこっちを眺めてみようかと思う。