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 |
+-----------------------------------------------------------------------------------------+
まとめ
とりあえずサラッと触りだけやってみた。高速なのかどうか?はこれぐらいじゃわからないけれども、以下を見る限りは高速みたい。
ドキュメントはめちゃめちゃ豊富、というかめちゃめちゃ多いので、一通り機能的なところをカバーしてみよう、みたいなのは諦めた。具体的にこういうことがやりたい、とか、こういうときはどうすれば?みたいな感じなら、だいたい書いてありそうな雰囲気を感じた。
というわけでこっちを眺めてみようかと思う。
久々に動かしてみたら、なんか以前と多少変わってるような気がした。一応メモ。
calm3をOpenAI互換APIで動かす
uv init -p 3.12.9 vllm-calm3 && cd vllm-calm3
uv add vllm bitsandbytes
(snip)
+ bitsandbytes==0.45.5
(snip)
+ vllm==0.8.5.post1
(snip)
uv run vllm serve cyberagent/calm3-22b-chat \
--host 0.0.0.0 \
--port 8888 \
--quantization bitsandbytes
ERROR 05-27 17:49:04 [engine.py:448] ValueError: The model's max seq len (16384) is larger than the maximum number of tokens that can be stored in KV cache (12528). Try increasing `gpu_memory_utilization` or decreasing `max_model_len` when initializing the engine.
エラーメッセージは--max_model_len
で入力コンテキストを減らすか、--gpu_memory_utilization
でもっとVRAM割り当てろ、と言っている。入力コンテキストは減らすとして、あと、KVキャッシュの量子化ができるみたい。このへんかな。
uv run vllm serve cyberagent/calm3-22b-chat \
--host 0.0.0.0 \
--port 8888 \
--quantization bitsandbytes \
--max_model_len 12528 \
--kv-cache-dtype fp8
Tue May 27 18:27:01 2025
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 560.35.05 Driver Version: 560.35.05 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 |
| 31% 48C P0 51W / 450W | 21791MiB / 24564MiB | 0% Default |
| | | N/A |
+-----------------------------------------+------------------------+----------------------+
これがギリギリかな。--gpu_memory_utilization
がいまいちよくわからない。
量子化してもVRAMはあるだけ使う(KVキャッシュのため)ってことなのかな?
これはどうやらそうみたい。ドキュメントの以下。
--gpu-memory-utilization
モデル実行に割り当てる GPU メモリの割合で、0 から 1 の範囲で指定できます。たとえば、0.5 を指定すると、GPU メモリの 50% が使用されます。指定しない場合、デフォルト値の 0.9 が使用されます。これはインスタンスごとの制限であり、現在の vLLM インスタンスにのみ適用されます。同じ GPU で別の vLLM インスタンスが実行されている場合でも、この設定は影響しません。例えば、同じ GPU で 2 つの vLLM インスタンスを実行している場合、各インスタンスの GPU メモリ使用率を 0.5 に設定できます。
デフォルト: 0.9
つまり、vLLMインスタンスを起動するとVRAMの90%がそのインスタンスに割り当てられる、でそのうち、モデルのロードに使用する分を除いた残りがKV Cache用に割り当てられる、--gpu-memory-utilization
でそのインスタンスに割り当てるVRAM量を設定できる、ということみたい。
たまたま見てみたらインストール手順が --torch-backend=auto
を指定するものになっていた。今風。
cyberagent/DeepSeek-R1-Distill-Qwen-14B-Japaneseで試してみる。
mkdir vllm-DeepSeek-R1-Distill-Qwen-14B-Japanese && cd $_
uv venv -p 3.12 --seed
uv pip install vllm --torch-backend=auto
uv pip install bitsandbytes
uv run vllm serve cyberagent/DeepSeek-R1-Distill-Qwen-14B-Japanese \
--host 0.0.0.0 \
--port 8000 \
--quantization bitsandbytes \
--max_model_len 32768 \
--kv-cache-dtype fp8
curl http://<サーバのIPアドレス>:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "cyberagent/DeepSeek-R1-Distill-Qwen-14B-Japanese",
"messages": [
{
"role": "user",
"content": "競馬の魅力について5つリストアップして"
}
]
}' | jq -r .choices[0].message.content
<think>
まず、ユーザーが求めているのは競馬の魅力についての5つのポイントをリストアップすることです。競馬はスポーツでありエンターテインメントでもあるので、その多面性を考慮する必要があります。
まず、競馬の歴史的背景を考えると、日本では江戸時代からある伝統的な競技です。歴史的な要素は文化的な魅力につながります。次に、競馬は単に馬が走るだけでなく、騎手や調教師の技術、馬の育成など、人間の努力が集約されたスポーツです。これも一つのポイントになります。
さらに、競馬はギャンブル要素も含んでいます。賭けが絡むことで、ドキドキ感や非日常的な体験を提供します。これはエンターテインメントとしての側面ですね。また、競馬場での観戦体験も重要です。馬券売り場やスタンドでの盛り上がりは、他のスポーツとは異なる雰囲気を生み出します。
最後に、競馬を通じて得られる学びや知見も挙げられます。例えば、馬の解剖学やレースの戦略、調教の技術など、知識が深まる点です。これらを整理して、5つのポイントにまとめる必要があります。重複しないように注意し、各項目が競馬の異なる側面を反映しているか確認します。
</think>
競馬の魅力を5つの観点から整理しました。伝統・競争・エンタメ・知性・社会性が交差する多面的な価値が特徴です。
1. **歴史の重み**
江戸時代の「競走」という言葉が起源で、日本独自の競技として約400年の歴史を持つ。現在も伝統的な「出走式」や「勝利騎手の親書」など、歴史的儀式が現代に継承され、文化としての重みを伝える。
2. **人間の極限対決**
騎手の体重制限(約57kg)と馬の体重(約500kg)のバランスが勝敗を分ける。機械的要因(馬の血統・調教)と人的要素(騎手の技術・臨機応変の判断)が拮抗する「人間と機械の最適化」が競馬の本質。
3. **ギャンブルの神経質**
単勝1万円配当(約100倍)という高還元率が「勝ち組心理」を喚起。観客の興奮は「リスク許容性」と「確率への執着」の二面性が生み出す。
4. **知的戦略の競演**
血統表(父・母の成績・世代)を分析する「血統研究」や、馬場の状態(土曜日は重馬場傾向)など、知識を活用した投資的観戦が可能。データ分析が勝敗を左右する「情報戦」としての側面。
5. **社会的交流の場**
競馬場では「馬券売り場での会話」「勝馬パドックの盛り上がり」など、観客同士の交流が生まれる。特に「オッズの推移」を共に追うという「集団的体験」がコミュニティ形成につながる。
これらの要素が融合することで、単なるスポーツを超えた「人間の情熱と理性の表現」として機能する。競馬の魅力は、伝統と現代性、個人の努力と運命の偶然が交差する「複合的なエンターテインメント」にこそあると言えるでしょう。