0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

KVキャッシュをQ4に落としたら32Kコンテキストが8GBに収まった — 壊れたのは数学だけだった

0
Posted at

KVキャッシュをQ4に落としたら32Kコンテキストが8GBに収まった — 壊れたのは数学だけだった

LLMの推論で最もVRAMを食うのはモデルの重み……ではない場合がある。

コンテキスト長が伸びると、KVキャッシュのメモリ消費がモデル本体を超える。Llama-3-8B(Q4_K_M, 4.9GB)で32Kコンテキストを使うと、KVキャッシュだけで約4GBを消費する。合計9GB。RTX 4060 8GBには入らない。

# KVキャッシュのメモリ計算
def kv_cache_memory(
    n_layers: int,
    n_heads_kv: int,
    head_dim: int,
    context_length: int,
    dtype_bytes: int = 2,  # FP16
) -> float:
    """KVキャッシュのメモリ使用量 (GB)"""
    # K + V の2つ分
    bytes_total = 2 * n_layers * n_heads_kv * head_dim * context_length * dtype_bytes
    return bytes_total / (1024 ** 3)

# Llama-3-8B (GQA: 8 KV heads)
llama3_8b = kv_cache_memory(
    n_layers=32,
    n_heads_kv=8,      # GQA: 32 attention heads → 8 KV heads
    head_dim=128,
    context_length=32768,  # 32K
    dtype_bytes=2,      # FP16
)
# → 4.0 GB

# Qwen2.5-32B (GQA: 8 KV heads)
qwen25_32b = kv_cache_memory(
    n_layers=64,
    n_heads_kv=8,
    head_dim=128,
    context_length=32768,
    dtype_bytes=2,
)
# → 8.0 GB — KVキャッシュだけで8GB。モデル本体を載せる余地がない

モデルを量子化してVRAMに収めても、KVキャッシュがFP16のままでは意味がない。コンテキスト長を伸ばした瞬間にVRAMが溢れる。

ここで登場するのがKVキャッシュの量子化だ。


KVキャッシュ量子化とは何か

モデル量子化との違い

モデルの重みの量子化(GGUF Q4_K_Mなど)はよく知られている。しかしKVキャッシュの量子化は根本的に異なる問題を解いている。

# 2種類の量子化の比較
quantization_types = {
    "モデル重みの量子化": {
        "対象": "学習済みパラメータ (offline)",
        "タイミング": "推論前に一度だけ変換",
        "精度影響": "広く研究済み。Q4_K_Mで大半のタスクは実用範囲",
        "ツール": "llama.cpp GGUF, GPTQ, AWQ, bitsandbytes",
        "VRAMへの効果": "モデルサイズに比例して削減",
    },
    "KVキャッシュの量子化": {
        "対象": "推論中に動的に生成されるアテンションの中間状態",
        "タイミング": "トークン生成のたびにリアルタイムで量子化",
        "精度影響": "研究が進行中。タスク依存性が高い",
        "ツール": "llama.cpp (--cache-type-k, --cache-type-v), vLLM (FP8)",
        "VRAMへの効果": "コンテキスト長に比例して削減",
    },
}

重要な違い: モデル重みの量子化は「静的なデータの圧縮」であり、推論前に一度行えばいい。KVキャッシュの量子化は「推論中にリアルタイムで生成されるデータの圧縮」であり、推論中に毎回発生する。だからオーバーヘッドがある。

llama.cppでの使い方

llama.cpp は --cache-type-k--cache-type-v オプションでKVキャッシュの量子化をサポートしている。

# FP16 KVキャッシュ (デフォルト)
llama-cli -m model.gguf -c 32768

# Q8_0 KVキャッシュ (メモリ半減、品質影響最小)
llama-cli -m model.gguf -c 32768 --cache-type-k q8_0 --cache-type-v q8_0

# Q4_0 KVキャッシュ (メモリ1/4、品質影響あり)
llama-cli -m model.gguf -c 32768 --cache-type-k q4_0 --cache-type-v q4_0

RTX 4060 8GBでのメモリ計算

Llama-3-8B Q4_K_M + KVキャッシュ量子化

# RTX 4060 8GB でのVRAM使用量見積もり
configs = {
    "FP16 KV (デフォルト)": {
        "model": 4.9,        # Q4_K_M
        "kv_32k": 4.0,       # FP16
        "overhead": 0.5,     # CUDAコンテキスト等
        "total": 9.4,
        "fits_8gb": False,
    },
    "Q8_0 KV": {
        "model": 4.9,
        "kv_32k": 2.0,       # FP16の半分
        "overhead": 0.5,
        "total": 7.4,
        "fits_8gb": True,    # ぎりぎり
    },
    "Q4_0 KV": {
        "model": 4.9,
        "kv_32k": 1.0,       # FP16の1/4
        "overhead": 0.5,
        "total": 6.4,
        "fits_8gb": True,    # 余裕あり
    },
}

# FP16だと32Kコンテキストは不可能
# Q8_0でぎりぎり
# Q4_0で1.6GBの余裕 → Embedding modelも同居可能

実用構成の比較

構成1: Llama-3-8B Q4_K_M + FP16 KV
  コンテキスト: ~16K が限界 (VRAM 7.4GB)
  用途: 短い会話、コード生成

構成2: Llama-3-8B Q4_K_M + Q8_0 KV
  コンテキスト: 32K まで可能 (VRAM 7.4GB)
  用途: 中程度の文書処理、長めの会話
  品質: FP16とほぼ同等 (後述)

構成3: Llama-3-8B Q4_K_M + Q4_0 KV
  コンテキスト: 32K (VRAM 6.4GB) → BGE-M3と同居可能
  用途: RAG + 長文コンテキスト
  品質: タスクによっては劣化が見える

構成4: Qwen2.5-32B Q4_K_M + Q4_0 KV (部分オフロード)
  モデル: 18GB → GPU 7.5GB + CPU 10.5GB
  KV (8K): Q4_0 で 0.5GB → GPU
  用途: 短コンテキストだが高品質な回答が必要な場合

KVキャッシュ量子化の本質: モデルサイズとコンテキスト長のトレードオフを変える。FP16 KVでは「小モデル × 短コンテキスト」しか選べなかったが、Q4_0 KVなら「小モデル × 長コンテキスト」や「大モデル × 短コンテキスト + RAG」の選択肢が開ける。


品質はどこで壊れるか

KIVI論文の知見

KVキャッシュ量子化の体系的な研究として、KIVI(Liu et al., 2024, arXiv:2402.02750)がある。

# KIVI: Key-Value Cache Quantization の主要な発見
kivi_findings = {
    "手法": "Key は per-channel 量子化、Value は per-token 量子化",
    "理由": {
        "Key": "チャネル間で値の分布が大きく異なる → per-channel が適切",
        "Value": "トークン間で値の分布が大きく異なる → per-token が適切",
    },
    "結果": {
        "2bit_KV": "KIVI方式 (per-channel K + per-token V) でdownstreamタスク精度の低下は最大2%程度",
        "2bit_KV_longbench": "LongBench: 44.27 vs FP16の44.52 (差0.56%)",
        "VRAM_savings": "2bit vs FP16でKVキャッシュ87.5%削減 (1/8)。論文全体では2.6倍のピークメモリ削減",
    },
    "重要な注意": "KとVで最適な量子化軸が異なる。同じ方法で両方を量子化すると品質が急落する",
}

KIVIの核心的な発見は、KとVの量子化は別々に設計すべきということだ。Keyの各チャネルの値域はトークンをまたいで安定しているが、チャネル間では大きく異なる。Valueはその逆。同じ方法で量子化すると、一方で精度が大きく崩れる。

どのタスクで壊れやすいか

# タスク別のKVキャッシュ量子化耐性
task_sensitivity = {
    "耐性が高い (Q4でも実用的)": [
        "単純なQ&A (事実検索)",
        "要約 (短→短)",
        "分類タスク",
        "コード補完 (短いコンテキスト)",
    ],
    "中程度 (Q8推奨)": [
        "長文要約 (16K+ トークン)",
        "多ターン会話 (10+ターン)",
        "RAGでの文書参照",
        "翻訳 (特に技術文書)",
    ],
    "耐性が低い (FP16推奨)": [
        "数学的推論 (CoT)",
        "正確な数値の引用",
        "長距離の情報参照 (needle-in-haystack)",
        "コードの論理的整合性 (長い関数)",
    ],
}

パターンが見える。情報の「正確な保持」が必要なタスクほど量子化に弱い。要約や分類は入力の「要旨」が残れば十分だが、数学やneedle-in-haystackは特定のビットの正確な値に依存する。

これはモデル重みの量子化と同じ傾向だ。Q4_K_Mで一般的な会話は問題ないが、数学のベンチマークでは劣化が見える。KVキャッシュ量子化でも同じ法則が効く。


実装パターン: コンテキスト長に応じた動的切り替え

理想的なのは、コンテキスト長に応じてKVキャッシュの量子化レベルを切り替えることだ。短いコンテキストではFP16で最高品質、長くなったらQ8_0やQ4_0に落として溢れを防ぐ。

# コンテキスト長に応じたKVキャッシュ設定の自動選択
def select_kv_config(
    model_vram_gb: float,
    gpu_vram_gb: float = 8.0,
    target_context: int = 32768,
    n_layers: int = 32,
    n_heads_kv: int = 8,
    head_dim: int = 128,
) -> dict:
    """利用可能なVRAMに基づいてKVキャッシュ設定を選択"""
    overhead = 0.5  # CUDAコンテキスト等
    available = gpu_vram_gb - model_vram_gb - overhead

    kv_sizes = {}
    for dtype, factor in [("f16", 2), ("q8_0", 1), ("q4_0", 0.5)]:
        bytes_per_token = 2 * n_layers * n_heads_kv * head_dim * factor
        max_ctx = int(available * (1024**3) / bytes_per_token)
        kv_sizes[dtype] = {
            "max_context": max_ctx,
            "vram_at_target": (bytes_per_token * target_context) / (1024**3),
        }

    # 最高品質で収まるものを選択
    for dtype in ["f16", "q8_0", "q4_0"]:
        if kv_sizes[dtype]["max_context"] >= target_context:
            return {
                "cache_type": dtype,
                "max_context": kv_sizes[dtype]["max_context"],
                "kv_vram": kv_sizes[dtype]["vram_at_target"],
                "total_vram": model_vram_gb + kv_sizes[dtype]["vram_at_target"] + overhead,
            }

    return {"cache_type": "q4_0", "max_context": kv_sizes["q4_0"]["max_context"],
            "note": "target_context exceeds available VRAM even with Q4_0"}

# Llama-3-8B Q4_K_M on RTX 4060 8GB
config = select_kv_config(model_vram_gb=4.9, target_context=32768)
# → {"cache_type": "q8_0", "max_context": ~33000, "kv_vram": 2.0, "total_vram": 7.4}
# Q8_0 でぎりぎり32Kに到達

# Qwen3.5-4B Q4_K_M on RTX 4060 8GB
# 注意: Qwen3.5-4Bはハイブリッドアーキテクチャ(32層中8層のみ通常Attention、残り24層はGated DeltaNet)
# 通常のKVキャッシュ計算は8注意層分のみ適用。モデルサイズ ~2.7GB
config_small = select_kv_config(model_vram_gb=2.7, n_layers=8, n_heads_kv=4, target_context=32768)
# → FP16でも32K が入る (2.7 + 0.5 + 0.5 = 3.7GB、8GBに大幅な余裕あり)

小さいモデルなら、KVキャッシュの量子化すら不要な場合がある。Qwen3.5-4B (~2.7GB) はハイブリッドアーキテクチャで注意層が8層のみのため、FP16 KVでも32Kコンテキストに十分な余裕がある。「大きいモデルを量子化で押し込む」より「小さい高精度モデルをフル精度で使う」方が正解になるケースがある。


llama.cppのKVキャッシュ量子化の実装詳細

Q8_0 vs Q4_0 の内部動作

# llama.cpp のKVキャッシュ量子化方式
kv_quant_details = {
    "q8_0": {
        "bit幅": 8,
        "ブロックサイズ": 32,
        "方式": "absmax symmetric quantization",
        "計算": "各32要素のブロックで max(abs(x)) を求め、スケールファクタを1つ保存",
        "精度": "FP16とほぼ同等(KIVI論文ではQ8でperplexity劣化は無視できるレベル)",
        "速度": "デコードはFP16と同等~やや速い (メモリ転送量が減る分)",
        "推奨": "デフォルトの代替として安全",
    },
    "q4_0": {
        "bit幅": 4,
        "ブロックサイズ": 32,
        "方式": "absmax symmetric quantization (4bit)",
        "計算": "各32要素を4bitに圧縮。スケールファクタ1つ",
        "精度": "タスク依存。単純なタスクでは問題ないが、数学・長距離参照で劣化",
        "速度": "帯域ボトルネック時はデコードが高速化する場合がある (転送量削減効果)",
        "推奨": "VRAM制約が厳しい場合のみ",
    },
}

デコード速度への影響

KVキャッシュ量子化で意外な副作用がある。メモリ帯域がボトルネックのデコード処理では、KVキャッシュが小さくなるとメモリ転送量が減り、デコードが速くなることがある。

# RTX 4060 (272 GB/s) でのデコード速度への影響概算
decode_impact = {
    "FP16 KV, 32K context": {
        "kv_read_per_token": "4.0 GB (全レイヤーのKV)",
        "理論速度": "272 / (4.9 + 4.0) = 30 t/s",
        "": "モデル重み + KV全体をアテンション計算で読む",
    },
    "Q8_0 KV, 32K context": {
        "kv_read_per_token": "2.0 GB",
        "理論速度": "272 / (4.9 + 2.0) = 39 t/s",
        "改善": "+30%",
    },
    "Q4_0 KV, 32K context": {
        "kv_read_per_token": "1.0 GB",
        "理論速度": "272 / (4.9 + 1.0) = 46 t/s",
        "改善": "+53%",
    },
}
# KVキャッシュの量子化で「VRAMが収まる」だけでなく「速くなる」
# ただし、dequantize のCPU/GPUオーバーヘッドがあるため、実測値はこれより低い

VRAMの節約だけでなく、帯域の節約にもなる。HBM4記事(別記事)で書いた「帯域の壁」に対して、KVキャッシュ量子化はLayer 2(読み出し量の削減)の一種として機能する。ハードウェアを変えずに実効帯域を上げる、ソフトウェア側の武器だ。


他のKVキャッシュ最適化との組み合わせ

KVキャッシュの量子化は単独で使うものではない。他の最適化と組み合わせた時に真価を発揮する。

手法                   VRAM削減   品質影響   実装難易度
──────────────────────────────────────────────────────
GQA (Grouped Query)    50-75%     なし       モデル設計時に決定
KV量子化 (Q8_0)        50%        ほぼなし   llama.cppフラグ1つ
KV量子化 (Q4_0)        75%        タスク依存 llama.cppフラグ1つ
Sliding Window         固定化     長距離劣化 モデル設計時に決定
Sparse Attention       大幅削減   タスク依存 カスタム実装が必要
Paged Attention        断片化防止 なし       vLLM等で自動

組み合わせ例: Llama-3-8B (GQA 4x) + Q8_0 KV
  GQAで75%削減 × Q8_0で50%削減 = ベースラインの12.5%
  FP16フルキャッシュ: 16GB → 2.0GB
  32Kコンテキストが8GBのGPUに余裕で収まる

GQA(Grouped Query Attention)は既にほとんどの最新モデルに組み込まれているため、ユーザーが意識する必要はない。Llama-3、Qwen2.5、Mistralはすべてこの恩恵を受けている。その上にKV量子化を重ねることで、さらに半分から1/4にできる。


8GBでは「量子化しない選択肢」もある

ここまでKVキャッシュ量子化の利点を書いてきたが、逆の視点も必要だ。

# KVキャッシュ量子化が不要なケース
unnecessary_cases = {
    "小モデル + 短コンテキスト": {
        "": "Qwen3.5-4B (~2.7GB, 注意層8のみ) + 8Kコンテキスト",
        "KV_FP16": "~0.13 GB (8注意層×4KVヘッド)",
        "合計": "~3.3 GB",
        "結論": "8GBの半分以下。量子化は無駄",
    },
    "タスク特化モデル": {
        "": "function calling専用 (短い入出力)",
        "KV_FP16": "< 0.1 GB",
        "結論": "コンテキストが短いならKVキャッシュは問題にならない",
    },
    "精度最優先タスク": {
        "": "数学推論、コードレビュー (正確性が最重要)",
        "結論": "モデルを小さくしてでもKVをFP16で保つ方が良い",
    },
}

マルチモデル記事(別記事)で書いた「タスクに応じてモデルを切り替える」戦略と組み合わせるのが現実的だ:

  • function calling → Qwen3.5-4B (~2.7GB) + FP16 KV。コンテキスト短い+注意層が少ないため量子化不要
  • 長文RAG → Llama-3-8B (4.9GB) + Q8_0 KV。32Kコンテキストで品質維持
  • 知識回答 → Qwen2.5-32B (18GB, CPU/GPUオフロード) + Q4_0 KV。短コンテキスト限定

全てのモデルに同じKV設定を適用するのではなく、タスクとコンテキスト長に応じて選ぶ。


参考文献

  1. "KIVI: A Tuning-Free KV Cache Quantization Plugin for Large Language Models" (2024) arXiv:2402.02750
  2. llama.cpp KV cache quantization: --cache-type-k, --cache-type-v options
  3. "PRISM: Breaking the O(n) Memory Wall in Long-Context LLM Inference via O(1) Photonic Block Selection" (2026) arXiv:2603.21576
  4. "Efficient Memory Management for Large Language Model Serving with PagedAttention" (2023) arXiv:2309.06180
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?