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 キャッシュがなぜ LLM 推論の次のボトルネックになるのか

0
Posted at

LLM の量子化モデルでは、モデルファイルが十分に小さくても、目的の文脈長や同時実行数で推論するとメモリが足りなくなることがあります。重みと KV キャッシュは別の領域だからです。

以前、量子化モデルの必要メモリを「重み」「KV キャッシュ」「その他の実行時領域」に分けて見積もる方法を整理しました[1]。また、prefill と decode では性能の決まり方が異なり、decode はメモリ帯域の影響を受けやすいことも整理しました[2]

本稿では、KV キャッシュに注目します。

結論から言うと、重みを低 bit 化して小さくした後、長い文脈や多数の同時リクエストを扱うと、KV キャッシュが次の容量・帯域制約として前面に出やすくなります。KIVI も、大きな batch size と長い 文脈長 では KV キャッシュのメモリ需要が増え、速度とメモリ使用量の新しいボトルネックになることを問題設定としています[3]

ここでいう「次のボトルネック」は、KV キャッシュが常に最大の制約になるという意味ではありません。重み側の制約を量子化などで緩和した後、文脈長や同時実行数を増やしたときに相対的重要性が上がる、という意味です。

この記事では、次の順で確認します。

  • KV キャッシュの容量を式で見積もる
  • config.json から計算に必要な値を確認する
  • 文脈長と同時系列数を増やしたときの増え方を見る
  • MHA、GQA、MQA で KV キャッシュ量がどう変わるか確認する
  • llama.cpp で KV キャッシュの型と context depth を変えて実測する
  • 容量、帯域、割り当ての 3 つを分けて判断する

KV キャッシュは重みとは別に増える

自己回帰型の LLM は、1 token を生成するたびに過去の token を参照します。過去の token について Key と Value を毎回最初から計算し直すのではなく、各 Transformer layer で計算済みの Key と Value を保存して再利用します。この保存領域が KV キャッシュです。

重みと KV キャッシュの違いを先に分けておくと、メモリ見積もりを整理しやすくなります。

領域 主に何で決まるか 推論中の増え方
モデル重み パラメータ数、重みの量子化形式 モデルをロードした後は基本的に固定
KV キャッシュ layer 数、文脈長、KV head 数、head dimension、キャッシュの型、保持する系列数 token や系列が増えると大きくなる
その他 バックエンド、バッチ、作業用バッファー、演算方式 実装と実行条件に依存

たとえば重みを Q4_K_M にしても、KV キャッシュまで自動的に 4 bit 相当になるわけではありません。現行の llama.cpp では K と V のキャッシュ型を --cache-type-k--cache-type-v で独立して指定でき、既定値はいずれも f16 です[4]

このため、「4 bit モデルだから推論時メモリもおおむね 4 bit 相当」と考えると見積もりを誤ります。

まず KV キャッシュ容量を式で見積もる

標準的な decoder-only Transformer を単純化すると、1 系列の KV キャッシュ容量は次の形で概算できます。

\mathrm{KV\ cache\ bytes}
=
2
\times L
\times T
\times H_{KV}
\times D
\times B

各記号は次の値です。

記号 意味
$L$ Transformer layer 数
$T$ KV キャッシュに保持する token 数
$H_{KV}$ Key / Value head 数
$D$ 1 head の次元数
$B$ 1 要素あたりの byte 数
先頭の 2 Key と Value の 2 種類

複数の独立した系列について同じ長さの KV を保持すると仮定する場合、理論上の保持データ量は系列数 $S$ を掛けて次のように考えられます。

\mathrm{KV\ cache\ bytes}
=
2
\times L
\times T
\times H_{KV}
\times D
\times B
\times S

ただし、実際の推論エンジンが確保する バッファーサイズは、連続領域の予約、共有、paging、unified KV、padding などに左右されます。後者の式は「保持すべき KV データ量」の概算として使い、実際の VRAM / RAM 使用量は実測で確認します。

32K context で 4 GiB になる例

次の条件を例にします。

num_hidden_layers    = 32
num_attention_heads  = 32
num_key_value_heads  = 8
head_dim             = 128
context_length       = 32768
KV キャッシュのデータ型 = F16

F16 は 1 要素 2 byte なので、1 系列の KV キャッシュは次の計算になります。

2
\times 32
\times 32768
\times 8
\times 128
\times 2
=
4294967296\ \mathrm{bytes}
=
4\ \mathrm{GiB}

この例では、32K context を 1 系列保持するだけで KV キャッシュは約 4 GiB です。

同じ構造で文脈長だけを変えると、概算は次のようになります。

文脈長 KV キャッシュ / 1 系列
4,096 token 約 0.5 GiB
8,192 token 約 1 GiB
16,384 token 約 2 GiB
32,768 token 約 4 GiB
65,536 token 約 8 GiB
131,072 token 約 16 GiB

他の条件を固定すれば、KV キャッシュ容量は文脈長に対して線形に増えます。

ここで重要なのは、モデルが 128K context に対応していることと、使用するハードウェアで 128K を実用的に確保できることは別だという点です。最大文脈長だけを見てモデルを選ぶのではなく、実際に使う文脈長で KV キャッシュを見積もる必要があります。

config.json から計算に必要な値を確認する

Hugging Face 形式のモデルであれば、まず config.json の Attention 関連値を確認します。

jq '{
  num_hidden_layers,
  hidden_size,
  num_attention_heads,
  num_key_value_heads,
  head_dim
}' config.json

典型的には次の値を使います。

  • num_hidden_layers: layer 数
  • num_attention_heads: Query head 数
  • num_key_value_heads: Key / Value head 数
  • hidden_size: hidden state の次元数
  • head_dim: model が明示している場合の 1 head の次元数

head_dim が設定に存在しない一般的な構成では、次の値を候補にできます。

D
=
\frac{\mathrm{hidden\_size}}
{\mathrm{num\_attention\_heads}}

ただし、これはすべてのモデルに適用できる規則ではありません。Key / Value の projection dimension が通常の MHA / GQA と異なるモデル、MLA を使うモデル、hybrid attention や特殊な head 構造を持つモデルでは、実装やモデル仕様を確認する必要があります。

Python で概算する

標準的な MHA / GQA モデルなら、次のスクリプトで config.json から F16 の KV キャッシュ容量を概算できます。

import argparse
import json

parser = argparse.ArgumentParser()
parser.add_argument("config")
parser.add_argument("--context", type=int, required=True)
parser.add_argument("--sequences", type=int, default=1)
parser.add_argument("--bytes-per-element", type=float, default=2.0)
args = parser.parse_args()

with open(args.config, encoding="utf-8") as f:
    config = json.load(f)

layers = config["num_hidden_layers"]
attention_heads = config["num_attention_heads"]
kv_heads = config.get("num_key_value_heads", attention_heads)

head_dim = config.get("head_dim")
if head_dim is None:
    head_dim = config["hidden_size"] // attention_heads

kv_bytes = (
    2
    * layers
    * args.context
    * kv_heads
    * head_dim
    * args.bytes_per_element
    * args.sequences
)

print(f"layers              : {layers}")
print(f"kv heads            : {kv_heads}")
print(f"head dim            : {head_dim}")
print(f"context             : {args.context}")
print(f"sequences           : {args.sequences}")
print(f"bytes per element   : {args.bytes_per_element}")
print(f"KV cache            : {kv_bytes / 1024**3:.2f} GiB")

たとえば config.json と 32K context を指定します。

python3 kv_cache_size.py config.json --context 32768

同じ条件で 4 系列分の保持データ量を見たい場合は次のようにします。

python3 kv_cache_size.py config.json \
  --context 32768 \
  --sequences 4

このスクリプトの --bytes-per-element は F32、F16、BF16 のように 1 要素あたりの byte 数を単純に置ける形式の概算に向いています。q8_0q4_0 などの block quantization では scale 等の付加情報があるため、10.5 を指定すれば実確保量と完全に一致するわけではありません。量子化 KV キャッシュは、後述の llama.cpp で実測します。

文脈長だけでなく同時に保持する系列数を見る

単一ユーザーが llama-cli を使う場合は 1 系列で問題にならなくても、推論サーバーでは複数の リクエストを並行して処理します。各リクエスト が長い文脈を持つと、同時に保持する KV データ量も増えます。

先ほどの「32K context で 1 系列 4 GiB」という条件を単純な理論量として並べると、次のようになります。

同時に保持する系列数 KV データ量の概算
1 約 4 GiB
2 約 8 GiB
4 約 16 GiB
8 約 32 GiB

これは「--parallel 8 を指定すれば必ず追加で 32 GiB の VRAM が確保される」という意味ではありません。現行の llama-server--parallel でサーバースロット数を指定でき、continuous batching(連続バッチ処理)と unified KV buffer も備えています[5]。実装側がどのようにバッファーを共有・予約するかによって、確保量と利用量は変わります。

実務では、まず「最大でどれだけの KV データを保持し得るか」をモデル構造から見積もり、その後に対象の推論エンジンで実測します。

たとえば llama-server では、条件を明示して起動できます。

llama-server \
  -m model.gguf \
  --ctx-size 32768 \
  --parallel 4 \
  --cache-type-k f16 \
  --cache-type-v f16

比較時はモデル、--ctx-size、KV キャッシュ型、GPU オフロード条件を固定し、--parallel だけを変えます。起動ログと GPU メモリ / Unified Memory の使用量を併記すると、理論上の KV データ量と実装上の確保量を混同せずに済みます。

MHA、GQA、MQA で KV キャッシュ量は大きく変わる

KV キャッシュ量の式には num_key_value_heads が入っています。この値を減らすのが MQA と GQA の重要な効果です。

Multi-Head Attention(MHA)では、各 Query head に対応する Key / Value head を持ちます。Multi-Query Attention(MQA)では Key / Value head を 1 組に共有します。Grouped-Query Attention(GQA)はその中間で、複数の Query head が少数の Key / Value head を共有します。GQA 論文は、MQA と MHA の中間数の Key / Value head を使う構成として GQA を定義しています[6]

たとえば Query head が 32 個のモデルで次の構成を比較します。

MHA : num_key_value_heads = 32
GQA : num_key_value_heads = 8
MQA : num_key_value_heads = 1

他の条件を固定すると、KV キャッシュ量の比率はそのまま次のようになります。

MHA : GQA : MQA
 32 :   8 :   1

先ほどの 32K context の例は num_key_value_heads = 8 なので約 4 GiB でした。これを同じ layer 数、head dimension、F16 のまま MHA の 32 KV heads にすると約 16 GiB になります。逆に MQA の 1 KV head なら約 0.5 GiB です。

この観点で見ると、GQA は単なる Attention の派生方式ではありません。decode 時に保持し、読み出す K/V を減らすアーキテクチャ上の選択でもあります。Grok-1 の Attention 構造を以前整理した際にも、Query head より Key / Value head を少なくする GQA と KV キャッシュの関係を扱いました[7]

重みを 4 bit にしても KV キャッシュは別に指定する

重み量子化と KV キャッシュ量子化は分けて考えます。

たとえば次の組み合わせは矛盾しません。

model weights : Q4_K_M
KV cache K    : f16
KV cache V    : f16

現行の llama.cpp では、K と V の型を個別に指定できます[4]

llama-cli \
  -m model-Q4_K_M.gguf \
  --ctx-size 32768 \
  --cache-type-k f16 \
  --cache-type-v f16

q8_0 を試すなら次のように変更します。

llama-cli \
  -m model-Q4_K_M.gguf \
  --ctx-size 32768 \
  --cache-type-k q8_0 \
  --cache-type-v q8_0

2026 年 8 月 19 日時点の llama.cpp CLI ドキュメント では、f32f16bf16q8_0q4_0q4_1iq4_nlq5_0q5_1 が K / V のキャッシュ型 として列挙されています[4]

KV キャッシュを低 bit 化すれば保持容量を減らせますが、低 bit にしただけで必ず高速になるとは限りません。量子化・逆量子化の演算、バックエンドが対応するカーネル、Flash Attention との組み合わせ、対象 GPU / CPU などによって結果は変わります。KIVI は KV キャッシュの 2 bit 量子化によってメモリ使用量を削減し、より大きなバッチを可能にする手法を示していますが、これは特定の手法と評価条件で得られた結果です[3]

そのため、KV キャッシュ型を変更したら容量と速度を別々に測ります。

llama-bench で context depth と KV キャッシュ型を分けて測る

現行の llama-bench では、-ctk-ctv で K / V のキャッシュ型 を指定できます。また、-d は指定した token 数まで KV キャッシュを事前に埋めた状態で ベンチマークを実行します[8]

まず F16 の KV キャッシュで、浅い状態と 4K、16K の context depth を比較します。

./llama-bench \
  -m model-Q4_K_M.gguf \
  -ctk f16 \
  -ctv f16 \
  -p 512 \
  -n 128 \
  -d 0,4096,16384 \
  -r 5

次に、他の条件を固定して q8_0 を比較します。

./llama-bench \
  -m model-Q4_K_M.gguf \
  -ctk q8_0 \
  -ctv q8_0 \
  -p 512 \
  -n 128 \
  -d 0,4096,16384 \
  -r 5

llama-bench は prompt processing を pp、text generation を tg として分けて測定できます[8]。本稿では特に tg と context depth の関係を見ます。

比較時に記録する項目は次の 5 つです。

項目 固定または記録する理由
llama.cpp の commit / version kernel や既定値の変更を区別する
モデル GGUF 重み形式の差を混ぜない
-ctk / -ctv KV キャッシュ型を明示する
-d KV キャッシュがどこまで埋まっているかを明示する
GPU オフロード / Flash Attention バックエンド条件の差を混ぜない

メモリ使用量は llama-bench の token/s とは別に、対象環境の GPU メモリ監視 や OS のメモリ監視で記録します。

ここで見るべきなのは、「q8_0 の方が速いか」だけではありません。

  • 同じ context depth でメモリ使用量がどれだけ変わるか
  • d=0 と長い context で tg がどう変わるか
  • キャッシュ型 を下げたときに速度が上がるか、下がるか、ほぼ変わらないか
  • 生成品質に実用上の差が出るか

を分けて判断します。

KV キャッシュは容量だけでなく decode のデータ移動量にも効く

KV キャッシュが問題になる理由は、保存容量だけではありません。

decode では新しい token を生成するたびに、Attention が過去の Key / Value を参照します。文脈が長くなるほど保持する K/V が増え、Attention が読み出す対象も増えます。GQA 論文でも、decoder inference において keys / values のロードがメモリ帯域の overhead になることが説明されています[6]

このため、KV キャッシュの問題は少なくとも 2 つに分けます。

観点 確認すること
Capacity 必要な KV キャッシュを VRAM / RAM に保持できるか
Bandwidth decode 中に必要な K/V を十分な速度で読み出せるか

重みの量子化にも同じような面があります。データ量を減らせば転送量は減りますが、対象ハードウェアがその形式を効率よく処理できるとは限りません。KV キャッシュでも「容量が半分になったから token/s も 2 倍になる」とは置けません。

実測では、容量と tg を別の列にして残す方が判断しやすくなります。

高並列 serving では KV キャッシュの「置き方」も制約になる

複数の リクエストを扱うと、各系列の長さは揃いません。ある request は短く終わり、別の request は長い文脈を保持し続けます。KV キャッシュを大きな連続領域として単純に予約すると、未使用領域や fragmentation が生じ、理論上の KV データ量とは別のメモリ損失が発生します。

PagedAttention はこの問題を、OS の 仮想メモリ と paging に着想を得た ブロック単位の KV キャッシュ管理 として扱います。PagedAttention を実装した vLLM の論文では、KV cache が リクエストごとに動的に増減し、fragmentation や重複によるメモリ浪費が batch size を制限することを問題として示しています[9]

ここまで来ると、KV キャッシュは単なる一時的な Tensor ではなく、推論サーバーが管理する実行時状態です。

そのため高並列 serving では、次の 3 つを分けて考えます。

  1. 情報量: モデル構造と文脈長から何 byte の K/V が必要か
  2. 転送量: decode 中に K/V をどれだけ読み出すか
  3. 管理効率: 複数リクエスト の K/V をどのように確保、共有、解放するか

「KV キャッシュを何 bit にするか」は 1 と 2 に効きます。「PagedAttention のようにどう配置するか」は主に 3 の問題です。同じ KV キャッシュ最適化でも、解いている問題が違います。

実際のモデルではこの順で判断する

モデルの必要メモリを確認するときは、モデルファイル容量だけで判断せず、次の順で確認すると切り分けやすくなります。

  1. config.json から layer 数、Attention head 数、KV head 数、head dimension を確認する
  2. 実際に使う 文脈長 を決める
  3. F16 / BF16 などの条件で KV キャッシュ理論量を計算する
  4. 同時に保持する系列数を想定する
  5. llama.cpp など推論エンジンの KV キャッシュ型 と割り当て方式を確認する
  6. 同一モデル、同一バックエンド で context depth と キャッシュ型 を変えて実測する
  7. メモリ使用量と tg を別々に比較する
  8. 必要なら 文脈長、同時実行数、モデル構造、KV キャッシュ型 の順に削減余地を探す

たとえば VRAM が不足した場合、重みをさらに Q4 から Q3 へ下げる前に、KV キャッシュが何 GiB を占めているか確認する価値があります。

長文脈が不要なら --ctx-size を現実の利用量まで下げる方が直接効きます。serving の同時実行数が過大なら --parallel の設計を見直せます。モデルを選び直せるなら、KV head 数の少ない GQA / MQA 系の構造は KV キャッシュ量に直接影響します。重み品質を維持したい場合は、重みではなく KV キャッシュ型を変更する余地もあります。

つまり、メモリ不足に対する選択肢は「モデル重みをもっと小さくする」だけではありません。

留意事項

KV キャッシュの単純式は、標準的な decoder-only Transformer の MHA / GQA を理解するには有効ですが、すべてのモデルにそのまま適用できるわけではありません。

特に次の条件ではモデル固有の構造を確認します。

  • MLA など K/V の保持方法そのものが異なる
  • Sliding Window Attention を使う layer がある
  • full attention と local attention を混在させる
  • Key / Value の dimension が通常の hidden_size / num_attention_heads と一致しない
  • cross-attention や multimodal 用の追加状態を持つ
  • 推論エンジンが KV を圧縮、共有、offload、paging する

また、batch sizeparallelslotsequencemicro batch は推論エンジンごとに意味が異なります。単純式の系列数と CLI option の値を機械的に同一視せず、対象実装の バッファー割り当て を確認します。

llama.cpp は継続的に更新されています。本稿で記載した キャッシュ型、既定値、--parallel、unified KV、llama-bench -d は 2026 年 8 月 19 日時点の公式ドキュメント で確認した内容です[4][5][8]。再測定するときは llama-cli --version などで ビルド情報または commit を記録し、同じ条件を再現できるようにします。

まとめ

重みを量子化すると、モデルファイルと重みの実行時占有量は小さくできます。しかし、KV キャッシュは別の条件で増えます。

標準的な MHA / GQA では、KV キャッシュ量は主に次の積で決まります。

layer 数
× 文脈長
× KV head 数
× head dimension
× キャッシュのデータ型
× 保持する系列数

重みはロード後にほぼ固定ですが、KV キャッシュは利用条件に応じて増える状態です。そのため、重みを小さくしたモデルほど、長文脈や高並列へ進んだときに KV キャッシュの比率が相対的に目立つ場合があります。

実務上は次の 3 点を分けて測れば判断しやすくなります。

  • 容量: 目的の文脈長 と系列数で KV キャッシュが何 GiB 必要か
  • 帯域: context depth が増えたとき tg がどう変化するか
  • 管理: 高並列時に KV キャッシュをどのように確保、共有、解放するか

モデルがメモリに載った時点でメモリ設計が終わるわけではありません。重みを小さくした後は、推論中に増える状態をどれだけ保持し、どれだけ速く読み、どう配置するかが次の設計対象になります

参考文献

  1. id774, LLM の量子化モデルで必要メモリと推論速度を見積もる方法(2026-08-08). https://qiita.com/ynakayama/items/0392abf8d2bceb74fecb
  2. id774, LLM の性能は prefill と decode で決まり方が違う(2026-08-13). https://qiita.com/ynakayama/items/247aa046f8804aa103d9
  3. Zirui Liu, Jiayi Yuan, Hongye Jin, Shaochen Zhong, Zhaozhuo Xu, Vladimir Braverman, Beidi Chen, Xia Hu, KIVI: A Tuning-Free Asymmetric 2bit Quantization for KV Cache(2024-02-05). https://arxiv.org/abs/2402.02750
  4. ggml-org, llama.cpp/tools/cli(2026-08-19). https://github.com/ggml-org/llama.cpp/blob/master/tools/cli/README.md
  5. ggml-org, LLaMA.cpp HTTP Server(2026-08-19). https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md
  6. Joshua Ainslie, James Lee-Thorp, Michiel de Jong, Yury Zemlyanskiy, Federico Lebrón, Sumit Sanghai, GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints(2023-05-22). https://arxiv.org/abs/2305.13245
  7. id774, Grok のアルゴリズムを詳解する(2026-07-16). https://blog.id774.net/entry/2026/07/16/5090/
  8. ggml-org, llama.cpp/tools/llama-bench(2026-08-19). https://github.com/ggml-org/llama.cpp/blob/master/tools/llama-bench/README.md
  9. Woosuk Kwon, Zhuohan Li, Siyuan Zhuang, Ying Sheng, Lianmin Zheng, Cody Hao Yu, Joseph E. Gonzalez, Hao Zhang, Ion Stoica, Efficient Memory Management for Large Language Model Serving with PagedAttention(2023-09-12). https://arxiv.org/abs/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?