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設定を適用するのではなく、タスクとコンテキスト長に応じて選ぶ。
参考文献
- "KIVI: A Tuning-Free KV Cache Quantization Plugin for Large Language Models" (2024) arXiv:2402.02750
- llama.cpp KV cache quantization:
--cache-type-k,--cache-type-voptions - "PRISM: Breaking the O(n) Memory Wall in Long-Context LLM Inference via O(1) Photonic Block Selection" (2026) arXiv:2603.21576
- "Efficient Memory Management for Large Language Model Serving with PagedAttention" (2023) arXiv:2309.06180