【LLM全般】ローカルLLMの推論速度を正確に測る!ベンチマーク手法と主要ツール完全ガイド

問題の概要:ローカルLLMの推論速度測定の難しさと課題

ローカル環境で大規模言語モデル(LLM)を実行する際、多くの開発者が直面する課題の一つが「推論速度の正確な測定」です。単に「速い」「遅い」という主観的な評価ではなく、定量的な指標で性能を比較・評価する必要があります。しかし、以下のような問題が発生することがあります。

  • 異なるハードウェア間での公平な比較が難しい
  • トークン生成速度(tokens/sec)の測定方法が統一されていない
  • プロンプト処理時間と生成時間を分離して計測できない
  • バッチ処理時のスループット測定が複雑
  • VRAM使用量やメモリ帯域のボトルネックを特定できない

具体的なエラーメッセージや問題例としては、以下のようなものがあります:

# よくある問題の例
WARNING: 推論中にOOM(Out Of Memory)エラーが発生
ERROR: CUDA out of memory. Tried to allocate 2.00 GiB

# 速度測定の不正確さ
INFO: 計測結果が実行ごとに大きくばらつく
測定点1: 45 tokens/sec
測定点2: 28 tokens/sec
測定点3: 52 tokens/sec

原因の解説:なぜ正確なベンチマークが難しいのか

ローカルLLMの推論速度測定が複雑な理由は、以下の要因が相互に影響し合うためです。

1. ハードウェア要因の多様性

GPUのVRAM容量・帯域幅、CPUのコア数・周波数、システムメモリの速度、ストレージのI/O性能など、多数の要素が推論速度に影響します。特に、モデルサイズがVRAMに収まるかどうかで、速度が劇的に変化します。

2. ソフトウェアスタックの複雑さ

LLMの推論エンジン(llama.cpp、vLLM、TensorRT-LLMなど)、量子化方式(GPTQ、AWQ、GGUF)、フレームワーク(PyTorch、ONNX Runtime)の選択によって、最適化レベルが異なります。

3. 測定条件の標準化不足

以下の条件を統一しないと、公平な比較ができません:

  • 入力トークン数と出力トークン数の比率
  • 温度パラメータやtop-pなどのサンプリング設定
  • コンテキストウィンドウのサイズ
  • キャッシュのウォームアップ有無

4. システムリソースの競合

バックグラウンドプロセスや他のアプリケーションによるCPU/GPU/メモリの使用状況が、測定結果に影響を与えます。

解決方法:体系的なベンチマーク手法と測定ツール

正確で再現性のあるベンチマークを行うためのステップバイステップの方法を紹介します。

ステップ1:測定環境の整備と標準化

まず、測定環境をクリーンな状態にし、条件を標準化します。

# システムリソースの確認コマンド(Linux例)
# GPU情報の確認
nvidia-smi
# CPU情報の確認
lscpu
# メモリ使用状況の確認
free -h
# バックグラウンドプロセスの一時停止(可能な場合)
sudo systemctl stop docker

ステップ2:適切なベンチマークツールの選択と導入

目的に応じて適切なツールを選択します。主要なツールは以下の通りです。

1. llama.cppのベンチマーク機能

llama.cppには組み込みのベンチマークツールが含まれており、様々なモデルと量子化形式に対応しています。

# llama.cppのビルドとベンチマーク実行例
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
make

# ベンチマークの実行(7Bモデル、Q4_K_M量子化)
./llama-bench -m ./models/llama-2-7b.Q4_K_M.gguf -n 128 -t 8

# 出力例
llama_print_timings:        load time =    1000.00 ms
llama_print_timings:      sample time =      50.00 ms
llama_print_timings: prompt eval time =    1500.00 ms / 128 tokens (   11.72 ms per token)
llama_print_timings:        eval time =   10000.00 ms / 127 tokens (   78.74 ms per token)
llama_print_timings:       total time =   11550.00 ms

2. LM Evaluation Harness(lm-eval)

推論速度だけでなく、モデルの精度も含めた総合的な評価が可能です。

# lm-evalのインストールと実行
pip install lm-eval

# ベンチマークの実行例
lm_eval --model hf 
    --model_args pretrained=meta-llama/Llama-2-7b-hf 
    --tasks hellaswag 
    --device cuda:0 
    --batch_size 8 
    --output_path ./results.json

# 速度関連の出力メトリクス
"runtime_info": {
    "samples_per_second": 15.2,
    "tokens_per_second": 305.7
}

3. vLLMのベンチマーク機能

バッチ処理のスループット測定に最適化されたツールです。

# vLLMのインストールとベンチマーク
pip install vllm

# ベンチマークスクリプトの実行例
python -m vllm.benchmarks.throughput 
    --model meta-llama/Llama-2-7b-hf 
    --dataset ShareGPT_V3_unfiltered_cleaned_split.json 
    --num-prompts 1000 
    --request-rate 10 
    --output results.csv

# 結果の解釈
Throughput: 125.4 requests/sec
Average latency: 45.2 ms/token
P95 latency: 78.9 ms/token

4. カスタム測定スクリプトの作成

特定のユースケースに合わせた測定を行う場合は、カスタムスクリプトが有効です。

import time
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

class LLMBenchmark:
    def __init__(self, model_name):
        self.tokenizer = AutoTokenizer.from_pretrained(model_name)
        self.model = AutoModelForCausalLM.from_pretrained(
            model_name,
            torch_dtype=torch.float16,
            device_map="auto"
        )
        
    def benchmark(self, prompt, max_new_tokens=100, num_runs=10):
        times = []
        tokens_generated = []
        
        inputs = self.tokenizer(prompt, return_tensors="pt").to("cuda")
        
        for i in range(num_runs):
            torch.cuda.synchronize()
            start_time = time.time()
            
            with torch.no_grad():
                outputs = self.model.generate(
                    **inputs,
                    max_new_tokens=max_new_tokens,
                    do_sample=False
                )
            
            torch.cuda.synchronize()
            end_time = time.time()
            
            generated_tokens = outputs[0][inputs['input_ids'].shape[1]:]
            num_tokens = len(generated_tokens)
            
            times.append(end_time - start_time)
            tokens_generated.append(num_tokens)
        
        avg_time = sum(times) / len(times)
        avg_tokens = sum(tokens_generated) / len(tokens_generated)
        tokens_per_sec = avg_tokens / avg_time
        
        return {
            "avg_time": avg_time,
            "avg_tokens": avg_tokens,
            "tokens_per_sec": tokens_per_sec,
            "std_dev_time": torch.std(torch.tensor(times)).item()
        }

# 使用例
benchmark = LLMBenchmark("meta-llama/Llama-2-7b-hf")
results = benchmark.benchmark(
    "日本の首都は",
    max_new_tokens=50,
    num_runs=20
)
print(f"平均速度: {results['tokens_per_sec']:.2f} tokens/sec")
print(f"時間の標準偏差: {results['std_dev_time']:.4f}秒")

ステップ3:測定条件の統一と記録

以下の条件を明記し、すべての測定で統一します。

# 測定条件テンプレート
測定条件:
- モデル: Llama-2-7b-chat-hf
- 量子化: GPTQ 4bit
- 入力トークン数: 128 tokens
- 出力トークン数: 128 tokens
- バッチサイズ: 1
- コンテキスト長: 4096
- 温度: 0.0 (貪欲サンプリング)
- ハードウェア: RTX 4090, 64GB RAM, Ryzen 9 7950X
- ソフトウェア: PyTorch 2.1, CUDA 11.8
- ウォームアップ実行: 3回
- 測定実行: 10回の平均

ステップ4:複数メトリクスの測定と分析

単一の数値ではなく、以下の複数のメトリクスを測定します。

  • Time to First Token (TTFT): 最初のトークンが生成されるまでの時間
  • Tokens per Second (TPS): 1秒あたりの生成トークン数
  • Inter-token Latency: トークン間の生成間隔
  • Peak Memory Usage: ピーク時のVRAM使用量
  • Throughput: バッチ処理時のスループット

コード例・コマンド例:実践的なベンチマークワークフロー

実際のワークフローを示す完全な例です。

#!/bin/bash
# 完全なベンチマークワークフロー例

echo "=== LLMベンチマークセットアップ開始 ==="

# 1. 環境情報の記録
echo "## システム情報" > benchmark_report.md
nvidia-smi >> benchmark_report.md
lscpu | grep "Model name" >> benchmark_report.md
free -h >> benchmark_report.md

# 2. モデルのダウンロード(例: llama-2-7bのGGUF形式)
echo "## モデルダウンロード" >> benchmark_report.md
wget https://huggingface.co/TheBloke/Llama-2-7B-GGUF/resolve/main/llama-2-7b.Q4_K_M.gguf 
    -O models/llama-2-7b.Q4_K_M.gguf 2>&1 >> benchmark_report.md

# 3. llama.cppでのベンチマーク実行
echo "## llama.cppベンチマーク結果" >> benchmark_report.md
cd llama.cpp
for threads in 4 8 16; do
    echo "### スレッド数: $threads" >> ../benchmark_report.md
    ./llama-bench 
        -m ../models/llama-2-7b.Q4_K_M.gguf 
        -n 256 
        -t $threads 
        -p "日本の人工知能研究について" >> ../benchmark_report.md
    echo "" >> ../benchmark_report.md
done

# 4. カスタムPythonスクリプトでの測定
echo "## Pythonスクリプトでの詳細測定" >> benchmark_report.md
cd ..
python3 custom_benchmark.py >> benchmark_report.md

echo "=== ベンチマーク完了 ==="
echo "結果は benchmark_report.md を確認してください"

まとめ・補足情報

ローカルLLMの推論速度を正確に測定するためには、体系的なアプローチが必要です。重要なポイントを以下にまとめます。

ベンチマークのベストプラクティス

  1. ウォームアップ実行を行う: 最初の数回の実行はキャッシュが効いていないため除外します。
  2. 十分な回数を測定する: 統計的有意性を得るために、少なくとも10回以上の測定を行います。
  3. システムリソースを監視する: nvidia-smiやhtopでリソース使用率を同時に監視します。
  4. 測定条件を詳細に記録する: 後で再現できるようにすべてのパラメータを記録します。

よくある落とし穴と回避策

問題: 測定結果が実行ごとに大きくばらつく
解決策: バックグラウンドプロセスを停止し、システムをクリーンな状態で測定します。また、測定回数を増やして平均値を取ります。

問題: 異なるハードウェア間での比較が難しい
解決策: 相対的な指標(例: 理論性能に対する達成率)を使用するか、同じモデルサイズに対する正規化された指標を開発します。

問題: 長時間実行時の性能低下
解決策: 長時間実行テストを実施し、メモリフラグメンテーションや熱スロットリングの影響を評価します。

将来の展望

LLM推論のベンチマークは急速に進化しています。特に以下の分野が注目されています:

  • 標準化されたベンチマークスイート: MLPerfのような業界標準の出現
  • リアルワールドワークロードのシミュレーション: 実際のユースケースに近い負荷テスト
  • エネルギー効率の測定: 性能だけでなく電力消費も考慮した評価
  • マルチモーダルモデルのベンチマーク: 画像や音声を含む総合的な評価

ローカルLLMの推論速度測定は、単なる「速さ」の競争ではなく、実際のアプリケーションでの実用性を評価する重要なプロセスです。適切なツールと方法論を用いて、公平で再現性のある測定を行うことで、モデル選択やハードウェア投資の意思決定をデータ駆動で行えるようになります。

この記事は役に立ちましたか?