Python LLM推論高速化実践:100msから10msレイテンシへの6つのプロダクションパターン

AI与大数据

Python LLM推論高速化実践:100msから10msレイテンシへの6つのプロダクションパターン

LLMのAPIレスポンスに3秒かかっている?ユーザーはページを閉じてしまう?GPU費用が毎月数万円?これは個別の問題ではありません。2026年、ほとんどのチームがLLMデプロイ後に直面する最初のボトルネックは推論レイテンシスループットです。モデル学習は全体の20%に過ぎず、推論最適化こそが本番運用の真の課題です。

本記事では、Python最新推論フレームワーク(vLLM 0.6+、TensorRT-LLM、llama.cpp)を用いた6つのプロダクション対応推論高速化パターンを紹介します。PagedAttentionから量子化デプロイまで、各パターンに完全な実行可能なPythonコードを付属します。


主要な学び

  • vLLM PagedAttentionの原理とプロダクション対応デプロイメント
  • TensorRT-LLMグラフ最適化とKernel Fusionの完全なワークフロー
  • GPTQ/AWQ/GGUF 3つの量子化手法の選定とデプロイ
  • KV Cache動的管理とPrefix Caching戦略の構築
  • Continuous BatchingによるGPU利用率3-5倍向上
  • 5つの最も一般的な推論デプロイの落とし穴を回避

目次

  1. LLM推論高速化アーキテクチャ概要
  2. パターン1:vLLM + PagedAttention効率推論
  3. パターン2:TensorRT-LLMグラフ最適化とKernel Fusion
  4. パターン3:量子化デプロイ(GPTQ/AWQ/GGUF)
  5. パターン4:KV Cache動的管理とPrefix Caching
  6. パターン5:Continuous Batchingと動的スケジューリング
  7. パターン6:本番環境デプロイとモニタリング
  8. 5つのよくある落とし穴と解決策
  9. 10のよくあるエラートラブルシューティング
  10. 高度な最適化テクニック
  11. 比較分析:4つの推論フレームワーク
  12. オンラインツールおすすめ
  13. まとめ

LLM推論高速化アーキテクチャ概要

LLM推論高速化は単一の技術ではなく、アルゴリズムからハードウェアまでの完全な最適化体系です:

┌─────────────────────────────────────────────────────────────────────┐
│                  LLM 推論高速化アーキテクチャ (2026)                    │
│                                                                     │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐      │
│  │ アルゴリズム│    │          │    │          │    │          │      │
│  │ レイヤー  │    │          │    │          │    │          │      │
│  │ 量子化    │───▶│ KV Cache │───▶│ バッチ   │───▶│ 推論     │      │
│  │ GPTQ     │    │ PagedAtt │    │ スケジュ │    │ エンジン │      │
│  │ AWQ      │    │ Prefix   │    │ Continuous│    │ vLLM     │      │
│  │ GGUF     │    │ SlidingW │    │ DynaBatch│    │ TRT-LLM  │      │
│  └──────────┘    └──────────┘    └──────────┘    └──────────┘      │
│                                                                     │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐      │
│  │ システム  │    │          │    │          │    │          │      │
│  │ レイヤー  │    │          │    │          │    │          │      │
│  │ モデル    │───▶│ 負荷分散  │───▶│ GPU      │───▶│ オブザー │      │
│  │ サービング│    │ Nginx LB │    │ スケジュ │    │ バビリティ│      │
│  │ FastAPI  │    │ Router   │    │ MIG      │    │ Prometheus│      │
│  │ Triton   │    │          │    │ MultiGPU │    │ Grafana  │      │
│  │ vLLM Srv │    │          │    │ TensorPar│    │ OTel     │      │
│  └──────────┘    └──────────┘    └──────────┘    └──────────┘      │
└─────────────────────────────────────────────────────────────────────┘

推論レイテンシの主要ボトルネック

ボトルネック段階 割合 説明 最適化方向
Prefill 30-40% 入力プロンプト処理、KV Cache計算 Flash Attention、Tensor並列
Decode 50-60% トークンごと生成、メモリ帯域制約 量子化、KV Cache最適化
スケジュールオーバーヘッド 5-10% リクエストキューイング、バッチ再編成 Continuous Batching
ネットワーク転送 5-15% APIリクエスト/レスポンスシリアライズ ストリーミング出力、圧縮

推論高速化技術ロードマップ

技術 レイテンシ改善 スループット改善 メモリ節約 実装難易度
vLLM PagedAttention 1.2x 2-4x 40-55%
TensorRT-LLM 2-3x 3-5x 20-30%
INT4量子化 1.5-2x 1.5-2x 60-75%
KV Cache最適化 1.3-1.5x 1.5-2x 30-50%
Continuous Batching 1.1x 3-5x 10-20%
Speculative Decoding 2-3x 0.8-1.2x -10%

💡 JSONフォーマッターツールを使って、推論APIのリクエスト/レスポンスJSON構造を素早く確認できます。


パターン1:vLLM + PagedAttention効率推論

vLLMのPagedAttentionは、2024-2026年のLLM推論分野における最も重要なイノベーションの一つです。従来の推論エンジンは各リクエストに固定サイズのKV Cacheを事前割り当てし、深刻なメモリ断片化と無駄を引き起こしていました。PagedAttentionはOS仮想メモリのページング機構を参考に、KV Cacheを固定サイズのBlockに分割し、オンデマンドで割り当て、メモリ利用率を40%から90%以上に向上させます。

1.1 PagedAttentionの原理

従来のKV Cache(事前割り当て):
  リクエスト1: [████████████░░░░░░░░]  2KB割り当て、1KB使用、50%無駄
  リクエスト2: [████░░░░░░░░░░░░░░░░]  2KB割り当て、0.5KB使用、75%無駄
  リクエスト3: [████████████████░░░░]  2KB割り当て、1.5KB使用、25%無駄
  断片:       ████████  利用不可能な断片空間

PagedAttention(ページ管理):
  Block Pool: [B0][B1][B2][B3][B4][B5][B6][B7][B8][B9]
  リクエスト1: B0→B1→B3  (3 Blockをオンデマンド割り当て)
  リクエスト2: B2→B5     (2 Blockをオンデマンド割り当て)
  リクエスト3: B4→B6→B7→B8 (4 Blockをオンデマンド割り当て)
  残り:       B9        (新リクエストに利用可能)
  → ゼロ断片、メモリ利用率90%+

1.2 vLLMクイックデプロイ

# vLLMインストール(CUDA 12.4+)
pip install vllm==0.6.6

# インストール確認
python -c "import vllm; print(vllm.__version__)"

1.3 基本推論サービス

from vllm import LLM, SamplingParams

def basic_inference():
    llm = LLM(
        model="Qwen/Qwen2.5-7B-Instruct",
        tensor_parallel_size=1,
        gpu_memory_utilization=0.90,
        max_model_len=8192,
        enforce_eager=True,
    )

    sampling_params = SamplingParams(
        temperature=0.7,
        top_p=0.9,
        max_tokens=1024,
        repetition_penalty=1.05,
    )

    prompts = [
        "PythonでO(1)のgetとput操作をサポートする効率的なLRUキャッシュを実装してください",
        "TransformerのMulti-Head Attentionの計算フローを説明してください",
        "RAGとFine-tuningの知識更新シナリオでの長所と短所を比較してください",
    ]

    outputs = llm.generate(prompts, sampling_params)

    for output in outputs:
        prompt = output.prompt
        generated_text = output.outputs[0].text
        print(f"Prompt: {prompt[:50]}...")
        print(f"Generated: {generated_text[:200]}...")
        print(f"Tokens: {len(output.outputs[0].token_ids)}")
        print("---")

if __name__ == "__main__":
    basic_inference()

1.4 OpenAI互換APIサーバー

# start_vllm_server.py
import subprocess
import os

def start_vllm_server():
    cmd = [
        "python", "-m", "vllm.entrypoints.openai.api_server",
        "--model", "Qwen/Qwen2.5-7B-Instruct",
        "--host", "0.0.0.0",
        "--port", "8000",
        "--tensor-parallel-size", "1",
        "--gpu-memory-utilization", "0.90",
        "--max-model-len", "8192",
        "--enable-prefix-caching",
        "--enable-chunked-prefill",
        "--max-num-seqs", "256",
        "--max-num-batched-tokens", "32768",
    ]

    env = os.environ.copy()
    env["CUDA_VISIBLE_DEVICES"] = "0"

    subprocess.run(cmd, env=env)

if __name__ == "__main__":
    start_vllm_server()

1.5 クライアント呼び出し

from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8000/v1",
    api_key="not-needed",
)

response = client.chat.completions.create(
    model="Qwen/Qwen2.5-7B-Instruct",
    messages=[
        {"role": "system", "content": "あなたはPythonの専門家です。簡潔かつ正確に回答してください。"},
        {"role": "user", "content": "Pythonで非同期イテレータを実装する方法は?"},
    ],
    temperature=0.7,
    max_tokens=1024,
    stream=True,
)

for chunk in response:
    if chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)
print()

パターン2:TensorRT-LLMグラフ最適化とKernel Fusion

TensorRT-LLMはNVIDIAの高性能推論エンジンで、計算グラフ最適化とKernel Fusionにより複数のGPU操作を単一カーネルに統合し、メモリアクセス回数とカーネル起動オーバーヘッドを大幅に削減します。

2.1 TensorRT-LLM最適化の原理

標準PyTorch推論(マルチカーネル):
  MatMul → GPU→CPU → LayerNorm → CPU→GPU → MatMul → GPU→CPU → Softmax
  ↑ 各カーネル起動約5-10μs、頻繁な切り替えでGPUアイドル発生

TensorRT-LLM(Kernel Fusion):
  [MatMul + LayerNorm + MatMul + Softmax] → 単一Fused Kernel
  ↑ 1回の起動で全計算完了、メモリアクセス90%削減

2.2 モデル変換とビルド

import tensorrt_llm
from tensorrt_llm import LLM, BuildConfig

def build_trt_engine():
    llm = LLM(
        model="Qwen/Qwen2.5-7B-Instruct",
        tensor_parallel_size=1,
        build_config=BuildConfig(
            max_input_len=2048,
            max_output_len=1024,
            max_batch_size=32,
            gpu_memory_utilization=0.90,
        ),
    )

    sampling_params = {
        "temperature": 0.7,
        "top_p": 0.9,
        "max_tokens": 1024,
    }

    output = llm.generate(
        prompts=["GPU Kernel Fusionの原理を説明してください"],
        sampling_params=sampling_params,
    )
    print(output)

if __name__ == "__main__":
    build_trt_engine()

2.3 FP8量子化高速化

from tensorrt_llm import LLM, BuildConfig
from tensorrt_llm.quantization import QuantConfig

def build_fp8_engine():
    quant_config = QuantConfig(
        quant_algo="FP8",
        calib_size=512,
    )

    llm = LLM(
        model="Qwen/Qwen2.5-7B-Instruct",
        tensor_parallel_size=1,
        quant_config=quant_config,
        build_config=BuildConfig(
            max_input_len=2048,
            max_output_len=1024,
            max_batch_size=32,
        ),
    )

    output = llm.generate(
        prompts=["FP8量子化がモデル精度に与える影響はどの程度ですか?"],
        sampling_params={"temperature": 0.7, "max_tokens": 512},
    )
    print(output)

if __name__ == "__main__":
    build_fp8_engine()

パターン3:量子化デプロイ(GPTQ/AWQ/GGUF)

量子化は推論コスト削減の最も直接的な手段です。モデル重みをFP16(16bit)からINT4(4bit)に圧縮すると、メモリ要件が75%削減され、推論速度が1.5-2倍向上します。

3.1 3つの量子化手法の比較

手法 精度損失 推論速度 メモリ節約 適用シナリオ
GPTQ 速い 75% GPUデプロイ、精度重視
AWQ 極小 最速 75% GPUデプロイ、速度重視
GGUF 調整可能 中程度 50-75% CPU/ハイブリッドデプロイ、柔軟

3.2 GPTQ量子化デプロイ

from vllm import LLM, SamplingParams

def gptq_inference():
    llm = LLM(
        model="Qwen/Qwen2.5-7B-Instruct-GPTQ-Int4",
        tensor_parallel_size=1,
        gpu_memory_utilization=0.85,
        quantization="gptq",
    )

    sampling_params = SamplingParams(
        temperature=0.7,
        max_tokens=512,
    )

    outputs = llm.generate(
        ["GPTQ量子化の原理は何ですか?"],
        sampling_params,
    )

    for output in outputs:
        print(output.outputs[0].text)

if __name__ == "__main__":
    gptq_inference()

3.3 AWQ量子化デプロイ

from vllm import LLM, SamplingParams

def awq_inference():
    llm = LLM(
        model="Qwen/Qwen2.5-7B-Instruct-AWQ",
        tensor_parallel_size=1,
        gpu_memory_utilization=0.85,
        quantization="awq",
    )

    sampling_params = SamplingParams(
        temperature=0.7,
        max_tokens=512,
    )

    outputs = llm.generate(
        ["AWQ量子化はGPTQに比べてどんな利点がありますか?"],
        sampling_params,
    )

    for output in outputs:
        print(output.outputs[0].text)

if __name__ == "__main__":
    awq_inference()

3.4 GGUF + llama.cppデプロイ(CPU/ハイブリッド推論)

import subprocess
import requests
import json

def start_llamacpp_server():
    cmd = [
        "./llama-server",
        "-m", "qwen2.5-7b-instruct-q4_k_m.gguf",
        "--host", "0.0.0.0",
        "--port", "8080",
        "-ngl", "32",
        "-c", "8192",
        "--parallel", "4",
        "-tb", "512",
    ]
    subprocess.Popen(cmd)

def query_llamacpp():
    response = requests.post(
        "http://localhost:8080/v1/chat/completions",
        json={
            "model": "qwen2.5-7b-instruct-q4_k_m.gguf",
            "messages": [
                {"role": "user", "content": "GGUF形式はどのようなデプロイシナリオに適していますか?"}
            ],
            "temperature": 0.7,
            "max_tokens": 512,
            "stream": True,
        },
        stream=True,
    )

    for line in response.iter_lines():
        if line:
            data = json.loads(line.decode("utf-8").removeprefix("data: "))
            if "choices" in data and data["choices"][0].get("delta", {}).get("content"):
                print(data["choices"][0]["delta"]["content"], end="", flush=True)
    print()

if __name__ == "__main__":
    start_llamacpp_server()
    import time
    time.sleep(10)
    query_llamacpp()

パターン4:KV Cache動的管理とPrefix Caching

KV CacheはLLM推論の最大メモリ消費者です。7Bモデルが8Kコンテキストを処理する場合、KV Cacheだけで4-6GBを消費します。適切なKV Cache管理がスループット向上の鍵です。

4.1 KV Cacheメモリ計算

def calculate_kv_cache_memory(
    num_layers: int,
    num_heads: int,
    head_dim: int,
    seq_len: int,
    batch_size: int,
    dtype_bytes: int = 2,
) -> int:
    kv_cache_per_token = num_layers * 2 * num_heads * head_dim * dtype_bytes
    total_memory = kv_cache_per_token * seq_len * batch_size
    return total_memory

qwen25_7b_kv = calculate_kv_cache_memory(
    num_layers=28,
    num_heads=28,
    head_dim=128,
    seq_len=8192,
    batch_size=32,
    dtype_bytes=2,
)
print(f"Qwen2.5-7B KV Cache (8K×32batch): {qwen25_7b_kv / 1024**3:.2f} GB")

qwen25_72b_kv = calculate_kv_cache_memory(
    num_layers=80,
    num_heads=64,
    head_dim=128,
    seq_len=8192,
    batch_size=32,
    dtype_bytes=2,
)
print(f"Qwen2.5-72B KV Cache (8K×32batch): {qwen25_72b_kv / 1024**3:.2f} GB")

4.2 Prefix Caching(システムプロンプトキャッシュ)

from vllm import LLM, SamplingParams
from vllm.prefix import Prefix

def prefix_caching_example():
    llm = LLM(
        model="Qwen/Qwen2.5-7B-Instruct",
        enable_prefix_caching=True,
        gpu_memory_utilization=0.90,
    )

    system_prompt = """あなたはプロのPythonコードレビュー専門家です。あなたの責任は:
1. コード内の潜在的なバグとセキュリティ脆弱性をチェックする
2. コードの可読性と保守性を評価する
3. 具体的な最適化提案を行う
4. 修正後のコード例を提供する

上記4つの観点から厳格にレビューしてください。"""

    prefix = Prefix(llm, system_prompt)

    sampling_params = SamplingParams(temperature=0.7, max_tokens=1024)

    prompts = [
        prefix + "\n\n以下のコードをレビューしてください:\n```python\ndef add(a, b): return a + b\n```",
        prefix + "\n\n以下のコードをレビューしてください:\n```python\ndef divide(a, b): return a / b\n```",
        prefix + "\n\n以下のコードをレビューしてください:\n```python\ndef factorial(n): return 1 if n <= 1 else n * factorial(n-1)\n```",
    ]

    outputs = llm.generate(prompts, sampling_params)
    for output in outputs:
        print(output.outputs[0].text[:200])
        print("---")

if __name__ == "__main__":
    prefix_caching_example()

4.3 Sliding Window Attention

from vllm import LLM, SamplingParams

def sliding_window_inference():
    llm = LLM(
        model="Qwen/Qwen2.5-7B-Instruct",
        gpu_memory_utilization=0.90,
        max_model_len=32768,
        sliding_window=4096,
    )

    sampling_params = SamplingParams(
        temperature=0.7,
        max_tokens=512,
    )

    long_prompt = "これは非常に長いテキストです..." * 2000

    outputs = llm.generate(
        [f"以下のテキストの要点をまとめてください:\n{long_prompt}"],
        sampling_params,
    )

    for output in outputs:
        print(output.outputs[0].text[:200])

if __name__ == "__main__":
    sliding_window_inference()

パターン5:Continuous Batchingと動的スケジューリング

従来のStatic Batchingは最も遅いリクエストの完了を待ってから次のバッチを処理するため、GPU利用率は通常30-50%に留まります。Continuous Batchingは新リクエストの随時参加と完了リクエストの随時退出を可能にし、GPU利用率を90%以上に向上させます。

5.1 Static vs Continuous Batching

Static Batching(最も遅いものを待つ):
  時間→  t1    t2    t3    t4    t5    t6
  リクエスト1: [████████████████]         ← 16トークン生成
  リクエスト2: [████]                     ← 4トークン生成、その後待機!
  リクエスト3: [████████]                 ← 8トークン生成、その後待機!
  GPU利用率: ~50%

Continuous Batching(動的スケジューリング):
  時間→  t1    t2    t3    t4    t5    t6
  リクエスト1: [████████████████]
  リクエスト2: [████]──リクエスト4: [████████]
  リクエスト3: [████████]──リクエスト5: [████]
  GPU利用率: ~90%

5.2 vLLM Continuous Batching設定

from vllm import LLM, SamplingParams
import asyncio
import time
from typing import List

async def continuous_batching_benchmark():
    llm = LLM(
        model="Qwen/Qwen2.5-7B-Instruct",
        tensor_parallel_size=1,
        gpu_memory_utilization=0.90,
        max_num_seqs=256,
        max_num_batched_tokens=32768,
        enable_chunked_prefill=True,
    )

    prompts_short = [
        f"{i}を一文で説明してください。"
        for i in ["再帰", "クロージャ", "コルーチン", "デコレータ", "ジェネレータ"]
    ]

    prompts_long = [
        f"{i}の原理、応用例、コード例を500字以上で説明してください。"
        for i in ["Transformerアーキテクチャ", "分散合意", "コンパイラ最適化"]
    ]

    all_prompts = prompts_short + prompts_long

    sampling_params = SamplingParams(
        temperature=0.7,
        max_tokens=512,
    )

    start_time = time.time()
    outputs = llm.generate(all_prompts, sampling_params)
    elapsed = time.time() - start_time

    total_tokens = sum(len(o.outputs[0].token_ids) for o in outputs)
    throughput = total_tokens / elapsed

    print(f"Total prompts: {len(all_prompts)}")
    print(f"Total tokens: {total_tokens}")
    print(f"Elapsed: {elapsed:.2f}s")
    print(f"Throughput: {throughput:.1f} tokens/s")

if __name__ == "__main__":
    asyncio.run(continuous_batching_benchmark())

5.3 動的バッチスケジューラ

import asyncio
import time
from dataclasses import dataclass, field
from typing import List, Optional
from collections import deque

@dataclass
class InferenceRequest:
    request_id: str
    prompt: str
    max_tokens: int = 512
    temperature: float = 0.7
    arrival_time: float = field(default_factory=time.time)
    completed: bool = False

class DynamicBatchScheduler:
    def __init__(
        self,
        max_batch_size: int = 32,
        max_waiting_time: float = 0.1,
        max_batch_tokens: int = 32768,
    ):
        self.max_batch_size = max_batch_size
        self.max_waiting_time = max_waiting_time
        self.max_batch_tokens = max_batch_tokens
        self.pending_queue: deque[InferenceRequest] = deque()
        self.running_batch: List[InferenceRequest] = []

    def add_request(self, request: InferenceRequest):
        self.pending_queue.append(request)

    def get_next_batch(self) -> List[InferenceRequest]:
        if not self.pending_queue:
            return []

        batch = []
        current_tokens = 0
        oldest_arrival = self.pending_queue[0].arrival_time

        while self.pending_queue and len(batch) < self.max_batch_size:
            wait_time = time.time() - oldest_arrival
            if wait_time >= self.max_waiting_time and batch:
                break

            request = self.pending_queue[0]
            estimated_tokens = len(request.prompt.split()) + request.max_tokens

            if current_tokens + estimated_tokens > self.max_batch_tokens:
                if batch:
                    break
                estimated_tokens = self.max_batch_tokens

            self.pending_queue.popleft()
            batch.append(request)
            current_tokens += estimated_tokens

        return batch

    @property
    def queue_size(self) -> int:
        return len(self.pending_queue)

scheduler = DynamicBatchScheduler(max_batch_size=32, max_waiting_time=0.1)

for i in range(50):
    scheduler.add_request(InferenceRequest(
        request_id=f"req-{i}",
        prompt=f"概念{i}を説明してください",
    ))

batch = scheduler.get_next_batch()
print(f"Batch size: {len(batch)}")
print(f"Remaining in queue: {scheduler.queue_size}")

パターン6:本番環境デプロイとモニタリング

6.1 Dockerデプロイ for vLLM

FROM vllm/vllm-openai:v0.6.6

ENV MODEL_NAME=Qwen/Qwen2.5-7B-Instruct
ENV TENSOR_PARALLEL_SIZE=1
ENV GPU_MEMORY_UTILIZATION=0.90
ENV MAX_MODEL_LEN=8192

EXPOSE 8000

ENTRYPOINT ["python", "-m", "vllm.entrypoints.openai.api_server"]
CMD ["--model", "${MODEL_NAME}", \
     "--host", "0.0.0.0", \
     "--port", "8000", \
     "--tensor-parallel-size", "${TENSOR_PARALLEL_SIZE}", \
     "--gpu-memory-utilization", "${GPU_MEMORY_UTILIZATION}", \
     "--max-model-len", "${MAX_MODEL_LEN}", \
     "--enable-prefix-caching", \
     "--enable-chunked-prefill"]
# docker-compose.yml
version: "3.8"

services:
  vllm-server:
    build: .
    ports:
      - "8000:8000"
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    environment:
      - MODEL_NAME=Qwen/Qwen2.5-7B-Instruct
      - TENSOR_PARALLEL_SIZE=1
      - GPU_MEMORY_UTILIZATION=0.90
    volumes:
      - ~/.cache/huggingface:/root/.cache/huggingface
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  prometheus:
    image: prom/prometheus:v2.52.0
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

  grafana:
    image: grafana/grafana:11.0.0
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin

6.2 Prometheus モニタリング設定

# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: "vllm"
    static_configs:
      - targets: ["vllm-server:8000"]
    metrics_path: /metrics
    scheme: http

6.3 推論パフォーマンスモニタリングメトリクス

import requests
import time
from dataclasses import dataclass
from typing import List

@dataclass
class InferenceMetrics:
    prompt_tokens: int
    completion_tokens: int
    total_tokens: int
    latency_ms: float
    tokens_per_second: float
    time_to_first_token_ms: float

def benchmark_inference(
    base_url: str = "http://localhost:8000",
    prompt: str = "Transformerアーキテクチャの原理を詳しく説明してください",
    max_tokens: int = 512,
    num_requests: int = 10,
) -> List[InferenceMetrics]:
    metrics_list = []

    for i in range(num_requests):
        start_time = time.time()
        ttft = None

        response = requests.post(
            f"{base_url}/v1/chat/completions",
            json={
                "model": "Qwen/Qwen2.5-7B-Instruct",
                "messages": [{"role": "user", "content": prompt}],
                "max_tokens": max_tokens,
                "temperature": 0.7,
                "stream": True,
            },
            stream=True,
        )

        full_content = ""
        for line in response.iter_lines():
            if not line:
                continue
            data = line.decode("utf-8")
            if data.startswith("data: "):
                data = data[6:]
            if data == "[DONE]":
                break

            import json
            chunk = json.loads(data)
            if chunk.get("choices") and chunk["choices"][0].get("delta", {}).get("content"):
                if ttft is None:
                    ttft = (time.time() - start_time) * 1000
                full_content += chunk["choices"][0]["delta"]["content"]

        elapsed_ms = (time.time() - start_time) * 1000
        completion_tokens = len(full_content)
        prompt_tokens = len(prompt.split()) * 2

        metrics = InferenceMetrics(
            prompt_tokens=prompt_tokens,
            completion_tokens=completion_tokens,
            total_tokens=prompt_tokens + completion_tokens,
            latency_ms=elapsed_ms,
            tokens_per_second=completion_tokens / (elapsed_ms / 1000) if elapsed_ms > 0 else 0,
            time_to_first_token_ms=ttft or 0,
        )
        metrics_list.append(metrics)

    avg_tps = sum(m.tokens_per_second for m in metrics_list) / len(metrics_list)
    avg_ttft = sum(m.time_to_first_token_ms for m in metrics_list) / len(metrics_list)
    avg_latency = sum(m.latency_ms for m in metrics_list) / len(metrics_list)

    print(f"Avg Throughput: {avg_tps:.1f} tokens/s")
    print(f"Avg TTFT: {avg_ttft:.0f} ms")
    print(f"Avg Latency: {avg_latency:.0f} ms")

    return metrics_list

if __name__ == "__main__":
    benchmark_inference()

5つのよくある落とし穴と解決策

落とし穴1:vLLM起動時OOM(メモリ不足)

現象torch.cuda.OutOfMemoryError: CUDA out of memory

原因gpu_memory_utilizationが高すぎる、またはmax_model_lenが大きすぎる

解決策

llm = LLM(
    model="Qwen/Qwen2.5-7B-Instruct",
    gpu_memory_utilization=0.85,
    max_model_len=4096,
    enforce_eager=True,
)

落とし穴2:量子化後の精度著しく低下

現象:INT4量子化後の出力が文字化けや論理的混乱

原因:GPTQキャリブレーションデータセットが実際の使用シーンと不一致

解決策:GPTQの代わりにAWQを使用、または一致するデータセットで再キャリブレーション

落とし穴3:KV Cacheメモリリーク

現象:長時間実行後のメモリ使用量継続増加

原因:リクエスト異常中断時のKV Cache未適切解放

解決策

# vLLMサーバー設定
cmd = [
    "--block-size", "16",
    "--swap-space", "4",
    "--disable-log-requests",
]

落とし穴4:TensorRT-LLMエンジンビルドに時間がかかりすぎる

現象:初回エンジンビルドに30分以上

原因:ビルド済みエンジンがディスクに保存されていない、毎回再コンパイル

解決策:エンジンをディスクに保存し、後続実行で直接ロード

落とし穴5:ストリーミング出力の高レイテンシ

現象:SSEストリーミング出力のチャンク間隔が500msを超える

原因:Chunked Prefillが無効、PrefillがDecodeをブロック

解決策

--enable-chunked-prefill \
--max-num-batched-tokens 32768

10のよくあるエラートラブルシューティング

エラーメッセージ 原因 解決方法
CUDA out of memory GPU メモリ不足 gpu_memory_utilizationまたはmax_model_lenを下げる
RuntimeError: Expected all tensors on the same device モデルとデータが異なるGPUに CUDA_VISIBLE_DEVICES設定を確認
ValueError: Token id out of range トークナイザとモデルの不一致 同じモデルのトークナイザを使用
ConnectionRefusedError: [Errno 111] vLLMサービス未起動 サービスプロセスとポートを確認
KeyError: 'model' リクエスト形式エラー OpenAI APIリクエスト形式を確認
AssertionError: block_size must be power of 2 block_sizeパラメータエラー 8/16/32に設定
RuntimeError: CUDA driver version is insufficient CUDAドライバが古い CUDA 12.4+にアップグレード
OSError: Model path not found モデルパスエラー HuggingFaceキャッシュまたはローカルパスを確認
TypeError: __init__() got an unexpected keyword argument vLLMバージョン不一致 vLLMバージョンとパラメータ互換性を確認
json.decoder.JSONDecodeError ストリーミングレスポンス解析エラー SSEデータ形式処理ロジックを確認

高度な最適化テクニック

テクニック1:Speculative Decoding(投機的デコーディング)

from vllm import LLM, SamplingParams

def speculative_decoding_example():
    llm = LLM(
        model="Qwen/Qwen2.5-7B-Instruct",
        speculative_model="Qwen/Qwen2.5-0.5B-Instruct",
        num_speculative_tokens=5,
        gpu_memory_utilization=0.90,
    )

    sampling_params = SamplingParams(temperature=0.0, max_tokens=512)

    outputs = llm.generate(
        ["量子コンピューティングの基本原理を説明してください"],
        sampling_params,
    )

    for output in outputs:
        print(f"Tokens: {len(output.outputs[0].token_ids)}")
        print(output.outputs[0].text[:200])

if __name__ == "__main__":
    speculative_decoding_example()

テクニック2:マルチGPU Tensor並列

from vllm import LLM, SamplingParams

def tensor_parallel_inference():
    llm = LLM(
        model="Qwen/Qwen2.5-72B-Instruct-AWQ",
        tensor_parallel_size=4,
        gpu_memory_utilization=0.90,
        max_model_len=8192,
    )

    sampling_params = SamplingParams(temperature=0.7, max_tokens=1024)

    outputs = llm.generate(
        ["大規模言語モデルの学習パイプラインを説明してください"],
        sampling_params,
    )

    for output in outputs:
        print(output.outputs[0].text[:200])

if __name__ == "__main__":
    tensor_parallel_inference()

テクニック3:LoRA動的ロード

from vllm import LLM, SamplingParams

def lora_inference():
    llm = LLM(
        model="Qwen/Qwen2.5-7B-Instruct",
        enable_lora=True,
        max_loras=4,
        max_lora_rank=16,
        gpu_memory_utilization=0.90,
    )

    sampling_params = SamplingParams(temperature=0.7, max_tokens=512)

    outputs = llm.generate(
        [
            {"prompt": "法的用語で契約の効力を説明してください", "lora_request": ("legal-lora", "/path/to/legal-lora", 16)},
            {"prompt": "医学用語で症状を説明してください", "lora_request": ("medical-lora", "/path/to/medical-lora", 16)},
        ],
        sampling_params,
    )

    for output in outputs:
        print(output.outputs[0].text[:200])

if __name__ == "__main__":
    lora_inference()

テクニック4:マルチモーダル推論高速化

from vllm import LLM, SamplingParams

def multimodal_inference():
    llm = LLM(
        model="Qwen/Qwen2.5-VL-7B-Instruct",
        limit_mm_per_prompt={"image": 1},
        gpu_memory_utilization=0.90,
    )

    sampling_params = SamplingParams(temperature=0.7, max_tokens=512)

    from vllm.inputs import PromptInputs
    inputs: PromptInputs = {
        "prompt": "<|image_pad|>この画像の内容を説明してください",
        "multi_modal_data": {"image": "https://example.com/photo.jpg"},
    }

    outputs = llm.generate([inputs], sampling_params)
    for output in outputs:
        print(output.outputs[0].text[:200])

if __name__ == "__main__":
    multimodal_inference()

テクニック5:推論結果キャッシュ

import hashlib
import json
import redis
from typing import Optional

class InferenceCache:
    def __init__(self, redis_url: str = "redis://localhost:6379", ttl: int = 3600):
        self.client = redis.from_url(redis_url)
        self.ttl = ttl

    def _cache_key(self, prompt: str, model: str, params: dict) -> str:
        raw = json.dumps({"prompt": prompt, "model": model, "params": params}, sort_keys=True)
        return f"llm:cache:{hashlib.sha256(raw.encode()).hexdigest()}"

    def get(self, prompt: str, model: str, params: dict) -> Optional[str]:
        key = self._cache_key(prompt, model, params)
        result = self.client.get(key)
        return result.decode("utf-8") if result else None

    def set(self, prompt: str, model: str, params: dict, response: str):
        key = self._cache_key(prompt, model, params)
        self.client.setex(key, self.ttl, response)

cache = InferenceCache()

cached = cache.get("Python GILを説明", "qwen2.5-7b", {"temperature": 0.7})
if cached:
    print(f"Cache hit: {cached[:100]}")
else:
    print("Cache miss, running inference...")
    cache.set("Python GILを説明", "qwen2.5-7b", {"temperature": 0.7}, "GILはグローバルインタプリタロック...")

比較分析:4つの推論フレームワーク

次元 vLLM TensorRT-LLM llama.cpp LMDeploy
初回トークンレイテンシ 最低
スループット 最高
メモリ効率 最高
量子化サポート GPTQ/AWQ/GGUF FP8/INT8 GGUF/Q4/Q5/Q8 AWQ/INT4
デプロイ難易度
コミュニティ 最も活発 NVIDIA公式 最も広範 商用サポート
マルチGPU ✅ Tensor並列 ✅ Pipeline+Tensor
ストリーミング出力
LoRA ✅ 動的ロード
適用シナリオ 汎用GPU推論 極限パフォーマンス CPU/エッジ 国産GPU

選定の推奨

  • 迅速なデプロイ:vLLM、すぐに使える、活発なコミュニティ
  • 最大パフォーマンス:TensorRT-LLM、NVIDIA GPU最適解
  • CPU/エッジデプロイ:llama.cpp + GGUF、GPU依存なし
  • 国産GPU対応:LMDeploy、Huawei Ascend等をサポート

💡 Base64エンコードツールを使って、推論APIでのバイナリデータ転送を処理できます。


オンラインツールおすすめ


まとめ

LLM推論高速化はシステム工学であり、アルゴリズム、システム、ハードウェアの3つのレイヤーで協調的な最適化が必要です。2026年の6つの主要プロダクションパターン:

  1. vLLM PagedAttention:メモリ利用率40%→90%+、推論高速化のインフラ
  2. TensorRT-LLM:Kernel Fusion + FP8量子化、極限パフォーマンスの選択
  3. 量子化デプロイ:GPU用GPTQ/AWQ、CPU/エッジ用GGUF、メモリ75%削減
  4. KV Cache管理:Prefix Cachingでシステムプロンプト再利用、Sliding Windowで長文脈処理
  5. Continuous Batching:動的バッチ処理、GPU利用率50%→90%
  6. プロダクションモニタリング:Prometheus + Grafanaフルスタックオブザーバビリティ、TTFT/TPSデュアルメトリクス駆動

将来のトレンド:Speculative Decodingは小モデル支援からモデル自己推測へ進化、FP8/INT4がデフォルト精度に、エッジ推論によりすべての開発者がローカルで大規模モデルを実行可能に。

LLM推論高速化で他の問題に遭遇した場合は、コメント欄で議論してください。役に立ったら、ブックマークとシェアをお忘れなく!


関連記事

ブラウザローカルツールを無料で試す →

#LLM推理加速#vLLM#TensorRT-LLM#量化部署#KV Cache#Python#2026#AI与大数据