第4章: LLMの推論と最適化

研究から本番環境へ:効率的なデプロイメント戦略

読了時間: 35-40分 難易度: 上級 最終更新: 2026年1月
免責事項: 本章は2026年初頭時点の最先端の推論最適化技術を解説しています。この分野は急速に進化しており、具体的なフレームワークやアプローチは変更される可能性があります。実装の詳細については常に公式ドキュメントを参照してください。

1. LLM推論の基礎

推論はLLMが価値を提供する場面です:テキスト生成、質問応答、アプリケーションの駆動など。しかし、推論には訓練とは異なる独自の課題があります。訓練は数時間から数日かけてスループットを最適化しますが、推論はミリ秒から秒単位で応答を返す必要があり、しばしば厳しいコスト制約の下で動作します。

1.1 推論パイプライン

flowchart LR A[入力テキスト] --> B[トークン化] B --> C[Prefillフェーズ] C --> D[Decodeフェーズ] D --> E[トークン生成] E -->|ループ| D E --> F[出力テキスト] style C fill:#e3f2fd style D fill:#fff3e0

2フェーズ推論

Prefillフェーズ(プロンプト処理):

Decodeフェーズ(トークン生成):

1.2 メモリ要件

def calculate_inference_memory(
    params_billions: float,
    context_length: int,
    batch_size: int,
    num_layers: int,
    hidden_dim: int,
    num_kv_heads: int,
    precision: str = "fp16"
) -> dict:
    """
    LLM推論のメモリ要件を計算

    Args:
        params_billions: モデルサイズ(10億パラメータ単位)
        context_length: 最大シーケンス長
        batch_size: 同時リクエスト数
        num_layers: Transformerレイヤー数
        hidden_dim: モデル隠れ次元
        num_kv_heads: KVアテンションヘッド数
        precision: 重みの精度

    Returns:
        メモリ内訳(GB単位)
    """
    bytes_per_param = {
        "fp32": 4, "fp16": 2, "bf16": 2,
        "int8": 1, "int4": 0.5, "fp8": 1
    }

    # モデル重み
    weight_bytes = params_billions * 1e9 * bytes_per_param[precision]
    weight_gb = weight_bytes / (1024**3)

    # KVキャッシュ: 2(KとV)× レイヤー × バッチ × シーケンス × ヘッド × ヘッド次元
    head_dim = hidden_dim // num_kv_heads
    kv_cache_bytes = (
        2 * num_layers * batch_size * context_length *
        num_kv_heads * head_dim * 2  # KVキャッシュはfp16
    )
    kv_cache_gb = kv_cache_bytes / (1024**3)

    # 活性化メモリ(概算)
    activation_gb = batch_size * context_length * hidden_dim * 4 / (1024**3)

    return {
        "重み(GB)": round(weight_gb, 2),
        "KVキャッシュ(GB)": round(kv_cache_gb, 2),
        "活性化(GB)": round(activation_gb, 2),
        "合計(GB)": round(weight_gb + kv_cache_gb + activation_gb, 2)
    }

# 例: 128Kコンテキストを持つLlama-3-70B
memory = calculate_inference_memory(
    params_billions=70,
    context_length=128000,
    batch_size=1,
    num_layers=80,
    hidden_dim=8192,
    num_kv_heads=8,  # GQA
    precision="int4"
)
print(f"メモリ内訳: {memory}")

1.3 レイテンシ構成要素

構成要素 説明 最適化対象
TTFT(最初のトークンまでの時間) 最初のトークンが出現するまでの遅延 Prefill最適化、プロンプトキャッシュ
TPOT(出力トークンあたりの時間) 生成トークンあたりの遅延 Decode最適化、バッチング
エンドツーエンドレイテンシ TTFT + (トークン数 × TPOT) 投機的デコーディング
スループット 全リクエストでのトークン/秒 継続的バッチング

2. モデル量子化

量子化は、モデルの精度を16ビットまたは32ビット浮動小数点から低ビット幅に削減し、メモリ使用量を大幅に削減し推論を高速化します。最新の量子化技術は、メモリを4-8倍削減しながらほぼ無損失の品質を達成します。

2.1 量子化の基礎

flowchart TB subgraph "精度レベル" FP32[FP32: 32ビット] --> FP16[FP16/BF16: 16ビット] FP16 --> FP8[FP8: 8ビット] FP8 --> INT8[INT8: 8ビット] INT8 --> INT4[INT4: 4ビット] end subgraph "メモリ削減" M1[100%] --> M2[50%] M2 --> M3[25%] M3 --> M4[25%] M4 --> M5[12.5%] end
import torch
import torch.nn.functional as F
from typing import Tuple

class Quantizer:
    """量子化の基本理解のためのユーティリティクラス"""

    @staticmethod
    def absmax_quantize(
        tensor: torch.Tensor,
        bits: int = 8
    ) -> Tuple[torch.Tensor, float]:
        """
        Absmax(対称)量子化
        値を[-2^(bits-1), 2^(bits-1)-1]にマップ
        """
        qmax = 2 ** (bits - 1) - 1
        scale = tensor.abs().max() / qmax
        quantized = torch.round(tensor / scale).to(torch.int8)
        return quantized, scale

    @staticmethod
    def block_quantize(
        tensor: torch.Tensor,
        block_size: int = 128,
        bits: int = 4
    ) -> Tuple[torch.Tensor, torch.Tensor]:
        """
        ブロック単位量子化(GPTQ、AWQで使用)
        各ブロックは独自のスケールファクターを持つ
        """
        original_shape = tensor.shape
        tensor_flat = tensor.view(-1)

        # block_sizeの倍数にパディング
        pad_len = (block_size - len(tensor_flat) % block_size) % block_size
        if pad_len:
            tensor_flat = F.pad(tensor_flat, (0, pad_len))

        blocks = tensor_flat.view(-1, block_size)

        # 各ブロックを独立して量子化
        qmax = 2 ** (bits - 1) - 1
        scales = blocks.abs().max(dim=1, keepdim=True).values / qmax
        scales = scales.clamp(min=1e-8)

        quantized = torch.round(blocks / scales).to(torch.int8)

        return quantized, scales.squeeze()

# デモンストレーション
tensor = torch.randn(1024, 1024)

int8_quant, int8_scale = Quantizer.absmax_quantize(tensor, bits=8)
int4_quant, int4_scales = Quantizer.block_quantize(tensor, bits=4)

print(f"元のサイズ: {tensor.numel() * 4 / 1024:.1f} KB")
print(f"INT8サイズ: {int8_quant.numel() * 1 / 1024:.1f} KB(4倍削減)")
print(f"INT4サイズ: {int4_quant.numel() * 0.5 / 1024:.1f} KB(8倍削減)")

2.2 量子化手法の比較

量子化手法比較(Llama-3-70B)

手法 ビット メモリ(GB) パープレキシティ FP16比速度
FP16(ベースライン) 16 140 5.42 1.0x
FP8 8 70 5.44 1.8x
GPTQ 4 35 5.58 2.2x
AWQ 4 35 5.52 2.4x

3. 高性能推論エンジン

最新の推論エンジンは、バッチング、メモリ管理、カーネル最適化、分散実行を含むサービングスタック全体を最適化します。2025-2026年の主要なソリューションはvLLMとTensorRT-LLMです。

3.1 vLLM: PagedAttention

vLLMはPagedAttentionを導入し、KVキャッシュを仮想メモリのページのように管理します。これによりメモリ断片化が解消され、効率的な継続的バッチングが可能になり、2-24倍のスループット向上を達成します。

flowchart TB subgraph "従来のKVキャッシュ" T1[リクエスト1: 2048トークン割り当て] T2[リクエスト2: 2048トークン割り当て] T3[無駄: 70%未使用] end subgraph "PagedAttention" P1[ページプール] P1 --> PA[リクエスト1: 5ページ使用] P1 --> PB[リクエスト2: 3ページ使用] P1 --> PC[空きページ: 動的割り当て] end style T3 fill:#ffcdd2 style PC fill:#c8e6c9
from vllm import LLM, SamplingParams
from typing import List

def setup_vllm_server(
    model: str,
    tensor_parallel_size: int = 1,
    gpu_memory_utilization: float = 0.9,
    max_model_len: int = 32768,
    quantization: str = None
) -> LLM:
    """
    高スループット推論用にvLLMを設定

    Args:
        model: モデル名またはパス
        tensor_parallel_size: テンソル並列のGPU数
        gpu_memory_utilization: 使用するGPUメモリの割合
        max_model_len: 最大シーケンス長
        quantization: "awq", "gptq", "fp8", またはNone
    """
    llm = LLM(
        model=model,
        tensor_parallel_size=tensor_parallel_size,
        gpu_memory_utilization=gpu_memory_utilization,
        max_model_len=max_model_len,
        quantization=quantization,
        # PagedAttention設定
        block_size=16,
        swap_space=4,  # CPUスワップ領域(GB)
        # 最適化フラグ
        enforce_eager=False,  # CUDAグラフを使用
        enable_prefix_caching=True,  # 共通プレフィックスをキャッシュ
    )
    return llm

def batch_inference(
    llm: LLM,
    prompts: List[str],
    max_tokens: int = 512,
    temperature: float = 0.7
) -> List[str]:
    """
    vLLMでバッチ推論を実行

    vLLMは自動的に以下を処理:
    - 継続的バッチング(動的なバッチ構成)
    - PagedAttention(効率的なKVキャッシュ管理)
    - プレフィックスキャッシュ(共通プレフィックスの再利用)
    """
    sampling_params = SamplingParams(
        max_tokens=max_tokens,
        temperature=temperature,
        top_p=0.95,
        stop=["", "[/INST]"],
    )

    outputs = llm.generate(prompts, sampling_params)
    return [output.outputs[0].text for output in outputs]

# 使用例
llm = setup_vllm_server(
    model="meta-llama/Llama-3.1-70B-Instruct",
    tensor_parallel_size=4,
    quantization="awq"
)

prompts = [
    "量子コンピューティングを簡単に説明してください。",
    "2つのソート済みリストをマージするPython関数を書いてください。",
]

results = batch_inference(llm, prompts)

3.2 推論エンジン比較

機能 vLLM TensorRT-LLM llama.cpp
最適用途 汎用サービング 最大スループット ローカル/エッジ展開
ハードウェア NVIDIA, AMD, Intel NVIDIAのみ CPU, GPU, Apple Silicon
セットアップ難易度 非常に低
相対スループット 1.0x 1.2-1.5x 0.3-0.5x

4. 長コンテキスト処理

コンテキストウィンドウは2023年の4Kトークンから2026年には100万トークン以上に劇的に拡大しました。これらの長いコンテキストを効率的に管理することは、最新の推論システムにおける重要な課題です。

4.1 コンテキスト長の進化

モデル コンテキスト長 リリース時期
GPT-4(初期) 8K / 32K 2023年3月
Claude 2 100K 2023年7月
Gemini 1.5 Pro 1M → 2M 2024年2月
Llama 4 Scout 10M 2025年4月

4.2 KVキャッシュ最適化技術

import torch
from typing import Tuple

class StreamingLLM:
    """
    StreamingLLM: アテンションシンクによる効率的な無限コンテキスト

    重要な洞察: 最初の数トークン(「アテンションシンク」)が
    グローバル情報を捕捉する。これらとスライディングウィンドウを
    保持することで無限ストリーミングが可能。
    """

    def __init__(
        self,
        num_sink_tokens: int = 4,
        window_size: int = 4096
    ):
        self.num_sink_tokens = num_sink_tokens
        self.window_size = window_size

    def evict_kv_cache(
        self,
        k_cache: torch.Tensor,
        v_cache: torch.Tensor,
        current_len: int
    ) -> Tuple[torch.Tensor, torch.Tensor]:
        """
        シンクと最近のウィンドウを保持しながら中間トークンを削除

        キャッシュレイアウト: [シンクトークン | 削除対象 | ウィンドウトークン]
        """
        max_len = self.num_sink_tokens + self.window_size

        if current_len <= max_len:
            return k_cache, v_cache

        # シンクトークンと最近のウィンドウを保持
        sink_k = k_cache[:, :, :self.num_sink_tokens, :]
        sink_v = v_cache[:, :, :self.num_sink_tokens, :]

        window_k = k_cache[:, :, -self.window_size:, :]
        window_v = v_cache[:, :, -self.window_size:, :]

        new_k = torch.cat([sink_k, window_k], dim=2)
        new_v = torch.cat([sink_v, window_v], dim=2)

        return new_k, new_v


class GroupedQueryAttention:
    """
    GQA: 複数のQueryヘッドで1つのKVヘッドを共有
    KVキャッシュサイズを大幅に削減
    """

    @staticmethod
    def calculate_kv_cache_size(
        batch_size: int,
        seq_len: int,
        num_layers: int,
        hidden_dim: int,
        num_q_heads: int,
        num_kv_heads: int,
        dtype_bytes: int = 2
    ) -> dict:
        """異なるAttention変種のKVキャッシュサイズを比較"""
        head_dim = hidden_dim // num_q_heads

        # MHA: num_kv_heads == num_q_heads
        mha_size = 2 * batch_size * seq_len * num_layers * num_q_heads * head_dim * dtype_bytes

        # GQA: num_kv_heads < num_q_heads
        gqa_size = 2 * batch_size * seq_len * num_layers * num_kv_heads * head_dim * dtype_bytes

        return {
            "MHA": f"{mha_size / 1e9:.2f} GB",
            "GQA": f"{gqa_size / 1e9:.2f} GB",
            "GQA削減率": f"{mha_size / gqa_size:.1f}x"
        }

# 例: Llama-3-70BのKVキャッシュ比較
kv_comparison = GroupedQueryAttention.calculate_kv_cache_size(
    batch_size=1,
    seq_len=128000,
    num_layers=80,
    hidden_dim=8192,
    num_q_heads=64,
    num_kv_heads=8  # 8 KVヘッドのGQA
)
print("KVキャッシュ比較:")
for k, v in kv_comparison.items():
    print(f"  {k}: {v}")

5. スループット最適化

5.1 継続的バッチング

従来の静的バッチングは、バッチ内のすべてのリクエストが完了するまで新しいリクエストを受け付けません。継続的(またはイン・フライト)バッチングは、リクエストの到着と完了に応じて動的に追加・削除し、GPU使用率を最大化します。

5.2 投機的デコーディング

投機的デコーディングは、小さな「ドラフト」モデルを使用して複数のトークンを提案し、大きな「ターゲット」モデルが並列で検証します。これにより自己回帰生成の2-3倍の高速化が可能です。

5.3 本番デプロイメントチェックリスト

推論最適化チェックリスト

メモリ最適化:

スループット最適化:

レイテンシ最適化:

まとめ

第4章の重要ポイント

前へ: LLMの訓練と整合性 次へ: 実践的LLMアプリケーション
English