皆さんこんにちは。
Acroquest のデータサイエンスチーム「AcroYAMALEX」を率いるチームリーダー、@tereka114です。
AcroYAMALEX では、コンペティション参加・自社製品開発・技術研究に日々取り組んでいます。チーム紹介はこちら。
本記事は、「学習推論ライブラリ・フレームワーク Advent Calendar 2025」の25日目です。
qiita.com
LLMは相変わらず新しいモデルが登場し世間を賑わせています。
しかし、モデル自体が重く、計算時間がかかります。そのため、LLMの推論では計算速度が重要となります。
「vLLM」では、LLMを効率的に推論する技術を用いて、推論の高速化を実現してきました。
※以前、本ブログでも次の記事で「vLLM」による高速化について紹介しました。
acro-engineer.hatenablog.com
今回は「vLLM」と同種のライブラリである「LMDeploy」の紹介をしつつ、速度を比較したいと思います。
LMDeploy
「LMDeploy」は、大規模言語モデル(LLM)や視覚言語モデル(VLM)を圧縮(quantization)・推論最適化・デプロイ・サービング(APIとして公開し、リクエストを受けて推論結果を返す運用)するためのオープンソースライブラリです。
NVIDIA FasterTransformerをベースに作られているTurboMind Engineをバックエンドとして動作しています。
LMDeploy の特徴
「LMDeploy」は以下の特徴があります。
高効率な推論(Efficient Inference)
「LMDeploy」は、Persistent Batch(リクエストを詰めて高スループット)、Blocked KV Cache(KVキャッシュのメモリ効率改善)、Tensor Parallelism(複数GPUに分割して推論)、高性能CUDAカーネル(演算の高速化)などで高効率な推論を実現します。
公式ドキュメントでは、これらの最適化により 「vLLM」比で最大1.8倍のリクエストスループットが得られると説明されています。
量子化に強い(Effective Quantization)
量子化(Quantization)とは、モデルの重みや推論時に使う内部データを FP16/FP32のような高精度表現から、INT8/INT4などの低ビット表現に置き換えて、メモリ使用量と計算量を減らす手法です。
一般に、GPUメモリ削減やスループット向上が期待できます(その代わり、設定によっては精度がわずかに低下することがあります)。
「LMDeploy」は、
- Weight-only量子化:モデルの「重み」だけを低ビット化して、主にモデルサイズと読み出し帯域を削減
- Key/Value量子化:AttentionのKVキャッシュも低ビット化して、長文や同時リクエスト時のメモリ消費を削減
に対応しています。公式には、4-bit推論でFP16比 最大2.4倍の性能が示されており、量子化後の品質はOpenCompass評価で確認されています。
本記事の比較では、代表例として AWQ量子化(Activation-aware Weight Quantization:重みの4-bit量子化方式の一つ。精度劣化を抑えやすく、配布済みのAWQモデルをそのまま推論に使えることが多い)を用いて性能を測定します。
分散サービングが手軽(Effortless Distribution Server)
リクエスト分配サービスを活用することで、複数マシン/複数GPUにまたがるマルチモデル推論サービスを、比較的シンプルにデプロイ・運用しやすい構成になっています。
リソースを増やすことで単純にさばける量を手軽に増やせることを意味します。
互換性が高い(Excellent Compatibility)
KV Cache Quant、AWQ、Automatic Prefix Caching を 同時に利用可能で、性能・メモリ・レイテンシの最適化をユースケースに応じて組み合わせることが可能です。
vLLMとの速度比較
「vLLM」と「LMDeploy」いずれもLLMサービング用途で使われるため、実運用では速度(特に同時リクエスト時の処理性能)が重要です。
そこで、いくつかの条件で性能を比較します。
性能計測の条件
性能を計測する条件を次の通り整理しました。
今回の計測モデルはQwen2.5-7B-Instructを利用しています。
データセットは、JGLUEに含まれているJSQuADを利用します。
JSQuADは問題とそれに関連する数段落分の文章が与えられるので、答えを回答するデータセットです。
今回はこのデータセットを処理する 総処理時間(秒。小さいほど高速) で比較します。
- 特筆するオプションなし(通常)
- 量子化(AWQ)
- Auto Prefix Caching
- AWQ + Auto Prefix Caching
また、計測対象のハードウェアは次のとおりです。
| 項目 | 値 |
|---|---|
| CPU | Core i9-12900 |
| メモリ | 128GB |
| GPU | RTX3090 x 1 |
計測結果
「vLLM」では、通常とAuto Prefix Cachingにおいて、「LMDeploy」と比較して高速であることがわかりました。
反面、「LMDeploy」はAWQでは、「vLLM」と比較して高速に動作しています。
この条件を元にすると、AWQでは「LMDeploy」が優位で、AWQ+Auto Prefix Cachingでも 今回の条件では僅差で「LMDeploy」が速いという結果でした。
| ライブラリ | 方式 | 1回目(秒) | 2回目(秒) | 3回目(秒) | 平均 |
|---|---|---|---|---|---|
| vLLM | 通常 | 291.16 | 291.2 | 290.7 | 291.02 |
| vLLM | AWQ | 319.27 | 319.3 | 319.45 | 319.34 |
| vLLM | Auto Prefix Caching | 173.49 | 174.88 | 174.23 | 174.20 |
| vLLM | AWQ+Auto Prefix Caching | 188.45 | 189.31 | 189.38 | 189.04 |
| LMDeploy | 通常 | 306.95 | 305.26 | 306.19 | 306.13 |
| LMDeploy | AWQ | 268.14 | 269.99 | 265.96 | 268.03 |
| LMDeploy | Auto Prefix Caching | 195.14 | 195.15 | 194.72 | 195.00 |
| LMDeploy | AWQ+Auto Prefix Caching | 186.20 | 186.52 | 188.91 | 187.21 |
計測実装
LMDeploy
import time import torch import pandas as pd from datasets import load_dataset from transformers import AutoTokenizer from lmdeploy import pipeline, TurbomindEngineConfig, GenerationConfig # ========================================== # 設定・定数 # ========================================== MODEL_NAME_BASE = "Qwen/Qwen2.5-7B-Instruct" MODEL_NAME_AWQ = "Qwen/Qwen2.5-7B-Instruct-AWQ" DATASET_NAME = "shunk031/JGLUE" SUBSET_NAME = "JSQuAD" # 生成パラメータ (ブログ記事のvLLM設定に準拠) GEN_CONFIG = GenerationConfig( max_new_tokens=1000, top_p=0.9, temperature=0.0 ) # ========================================== # データ準備関数 # ========================================== def prepare_prompts(model_name, enable_prefix_example=False): """ データセットを読み込み、プロンプトのリストを作成する """ print(f"Loading dataset and tokenizer for {model_name}...") dataset = load_dataset(DATASET_NAME, name=SUBSET_NAME) df = dataset["validation"].to_pandas() tokenizer = AutoTokenizer.from_pretrained(model_name) # Prefix Caching検証用の共通プロンプト (ブログ記事より引用) head_prompt = "" if enable_prefix_example: head_prompt = "\n例\nQuestion: 新たに造られた語のことを新語または何という?\nContext:造語 [SEP] 造語(ぞうご)は、新たに語(単語)を造ることや、既存の語を組み合わせて新たな意味の語を造ること、また、そうして造られた語である。新たに造られた語については、新語または新造語とも呼ばれる。\nOutput: 新造語" # プロンプトのフォーマット def format_content(x): return "\nQuestion:" + x["question"] + "\nContext: " + x["context"] + "\nOutput:" df["assistant_content"] = df.apply(format_content, axis=1) print("Formatting prompts...") prompts = [] # apply_chat_template を使用してチャット形式に変換 for content in df["assistant_content"].to_list(): messages = [ {"role": "system", "content": "Contextを参考にして、問題に回答してください。" + head_prompt}, {"role": "user", "content": content} ] # 文字列としてプロンプトを取得 prompts.append( tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) ) return prompts # ========================================== # 推論実行関数 # ========================================== def run_benchmark(config_name, model_path, engine_config, prompts): print(f"\n[{config_name}] Initializing pipeline...") # パイプラインの作成 # lmdeployは自動的にモデルをダウンロード・変換してTurboMindエンジンでロードします pipe = pipeline(model_path, backend_config=engine_config) print(f"[{config_name}] Starting inference on {len(prompts)} samples...") start_time = time.time() # 推論実行 (バッチ処理はlmdeployが内部で最適化して行います) responses = pipe(prompts, gen_config=GEN_CONFIG) end_time = time.time() elapsed_time = end_time - start_time print(f"[{config_name}] Total time: {elapsed_time:.2f} s") print(f"[{config_name}] Avg time per request: {elapsed_time / len(prompts):.4f} s") # メモリ解放 del pipe torch.cuda.empty_cache() # ========================================== # メイン処理 # ========================================== def main(): # 1. 通常推論 (FP16) # vLLMの初期設定に近い構成 prompts_normal = prepare_prompts(MODEL_NAME_BASE, enable_prefix_example=False) config_fp16 = TurbomindEngineConfig( dtype='float16', # 型指定 gpu_memory_utilization=0.95, # GPUメモリ使用率 session_len=4096 # コンテキスト長 (必要に応じて調整) ) run_benchmark("Normal (FP16)", MODEL_NAME_BASE, config_fp16, prompts_normal) # 2. AWQ量子化 # AWQモデルを使用。model_format='awq' は自動検出されることが多いですが明示も可能 # ※プロンプトはトークナイザが同じため再利用可能ですが、念のため同じものを使います config_awq = TurbomindEngineConfig( model_format='awq', # AWQフォーマット指定 gpu_memory_utilization=0.95 ) run_benchmark("AWQ Quantization", MODEL_NAME_AWQ, config_awq, prompts_normal) # 3. Auto Prefix Caching # 共通のPrefix (例題) を含んだプロンプトを作成して検証 prompts_prefix = prepare_prompts(MODEL_NAME_BASE, enable_prefix_example=True) config_prefix = TurbomindEngineConfig( dtype='float16', gpu_memory_utilization=0.95, enable_prefix_caching=True # Prefix Caching有効化 ) run_benchmark("Prefix Caching", MODEL_NAME_BASE, config_prefix, prompts_prefix) config_prefix = TurbomindEngineConfig( model_format='awq', # AWQフォーマット指定 enable_prefix_caching=True # Prefix Caching有効化 ) run_benchmark("AWQ + Prefix Caching", MODEL_NAME_AWQ, config_prefix, prompts_prefix) if __name__ == "__main__": main()
Dockerfile
# lmdeployの公式イメージを使用 (CUDA 12系推奨) FROM openmmlab/lmdeploy:latest # データセットのロードやプロンプト処理に必要なライブラリをインストール RUN pip install datasets pandas tqdm transformers accelerate # 作業ディレクトリの設定 WORKDIR /app
実行
docker build -t lmdeploy-benchmark .
docker run --gpus all --rm -it -v $(pwd)/main.py:/app/main.py -v ~/.cache/huggingface:/root/.cache/huggingface lmdeploy-benchmark python main.py
vLLM
実装(クリックで展開)
実装(Python)
import time import argparse import pandas as pd from datasets import load_dataset from transformers import AutoTokenizer from vllm import LLM, SamplingParams # ========================================== # 定数設定 # ========================================== MODEL_NAME_BASE = "Qwen/Qwen2.5-7B-Instruct" MODEL_NAME_AWQ = "Qwen/Qwen2.5-7B-Instruct-AWQ" DATASET_NAME = "shunk031/JGLUE" SUBSET_NAME = "JSQuAD" # ========================================== # データ準備 # ========================================== def prepare_prompts(model_name, enable_prefix_example=False): print(f"Loading dataset and tokenizer for {model_name}...") dataset = load_dataset(DATASET_NAME, name=SUBSET_NAME) df = dataset["validation"].to_pandas() tokenizer = AutoTokenizer.from_pretrained(model_name) # Prefix Caching検証用の共通プロンプト (ブログ記事準拠) head_prompt = "" if enable_prefix_example: head_prompt = "\n例\nQuestion: 新たに造られた語のことを新語または何という?\nContext:造語 [SEP] 造語(ぞうご)は、新たに語(単語)を造ることや、既存の語を組み合わせて新たな意味の語を造ること、また、そうして造られた語である。新たに造られた語については、新語または新造語とも呼ばれる。\nOutput: 新造語" # データ整形 df["assistant_content"] = df.apply( lambda x: "\nQuestion:" + x["question"] + "\nContext: " + x["context"] + "\nOutput:", axis=1 ) print("Formatting prompts...") prompts = [] # apply_chat_templateで整形 for content in df["assistant_content"].to_list(): messages = [ {"role": "system", "content": "Contextを参考にして、問題に回答してください。" + head_prompt}, {"role": "user", "content": content} ] prompts.append( tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) ) return prompts # ========================================== # メイン処理 # ========================================== def main(): parser = argparse.ArgumentParser() parser.add_argument("--scenario", type=str, required=True, choices=["normal", "awq", "prefix", "awq-prefix"], help="Benchmark scenario to run") args = parser.parse_args() # 設定変数 model_path = MODEL_NAME_BASE quantization = None enable_prefix_caching = False # シナリオごとの設定切り替え if args.scenario == "normal": print(">>> Running Scenario: Normal (FP16)") elif args.scenario == "awq": print(">>> Running Scenario: AWQ Quantization") model_path = MODEL_NAME_AWQ quantization = "awq" elif args.scenario == "prefix": print(">>> Running Scenario: Prefix Caching") enable_prefix_caching = True elif args.scenario == "awq-prefix": print(">>> Running Scenario: AWQ+Prefix Caching") model_path = MODEL_NAME_AWQ quantization = "awq" enable_prefix_caching = True # プロンプト準備 prompts = prepare_prompts(model_path, enable_prefix_example=enable_prefix_caching) # Samplingパラメータ (ブログ記事の設定に準拠) sampling_params = SamplingParams( temperature=0.0, top_p=0.9, max_tokens=1000 ) # vLLMエンジンの初期化 # ブログ記事の設定: gpu_memory_utilization=0.95, dtype='half', enforce_eager=True print("Initializing vLLM engine...") llm = LLM( model=model_path, quantization=quantization, gpu_memory_utilization=0.95, dtype='half', enforce_eager=True, # ブログ記事と同様にEager Executionモードを指定 enable_prefix_caching=enable_prefix_caching ) print(f"Starting inference on {len(prompts)} samples...") start_time = time.time() # 推論実行 (use_tqdm=Trueでプログレスバー表示) outputs = llm.generate(prompts, sampling_params, use_tqdm=True) end_time = time.time() elapsed_time = end_time - start_time print(f"[{args.scenario}] Total time: {elapsed_time:.2f} s") print(f"[{args.scenario}] Avg time per request: {elapsed_time / len(prompts):.4f} s") if __name__ == "__main__": main()
Dockerfile
# vLLMの公式イメージ (CUDA対応版) FROM vllm/vllm-openai:latest # データセット操作に必要なライブラリをインストール RUN pip install datasets pandas tqdm transformers accelerate # 作業ディレクトリ WORKDIR /app # エントリーポイントとしてbashを指定(手動実行しやすくするため) ENTRYPOINT ["/bin/bash"]
実行
docker build -t vllm-benchmark . docker run --gpus all --rm -it -v $(pwd)/main.py:/app/main.py -v ~/.cache/huggingface:/root/.cache/huggingface --ipc=host vllm-benchmark python3 main.py --scenario normal # awq or prefix
最後に
「LMDeploy」と「vLLM」の速度比較をいくつかの条件で実施しました。
AWQを利用した場合には「LMDeploy」を利用するのが、高速でした。しかし、特筆するオプションが無い場合、Auto Prefix Cachingを利用する場合には、「vLLM」が高速でした。
そのため、モデルをAWQで量子化して推論するときには、「LMDeploy」が速度として良好な可能性が高いです。
適切なライブラリを選ぶ際にはぜひ、こちらの実験結果を参考にしつつ、各々の条件で計測をしながら検討するのが良いでしょう。
Acroquest Technologyでは、キャリア採用を行っています。
- Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
- ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
- マイクロサービス、DevOps、最新のOSSやクラウドサービスを利用する開発プロジェクト
- 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。