KVキャッシュ圧縮 — 量子化・追い出し・共有で長文推論を軽くする
文脈を長くすると、GPUに載る同時実行数が減る。犯人はKVキャッシュで、その大きさは1本の掛け算式で決まります。式の項は3つしかないので、圧縮の方向も3つしかありません——精度を落とす、捨てる、共有する。KIVI・H2O・SnapKV・GQA・アテンションシンクを、その3項の上に並べ直します。
要点
- KVキャッシュのメモリは M = 2 × L × n × H_kv × d_h × b × B で決まり、文脈長にもリクエスト数にも比例して増える。
- 推論時に動かせる項は、精度 b、実際に保持するトークン数、KVヘッド数 H_kv の3つだけである。
- KIVIは、Keyには外れ値チャネルがありValueには無いという非対称性を使い、Kはチャネル方向、Vはトークン方向でまとめる。
- H2Oは累積注意でヘビーヒッターを残し、SnapKVはプロンプト末尾の票でprefill時に一括で削るが、追い出しは不可逆である。
- 先頭の数トークンはアテンションシンクとして使われるため、先頭を残すシンク+窓が定番である。
KIVI: A Tuning-Free Asymmetric 2bit Quantization for KV Cache
一次資料 — この記事の根拠
この解説の公開 2026-09-10
KIVI: A Tuning-Free Asymmetric 2bit Quantization for KV CachearXiv:2402.02750論文ページ·PDFH2O: Heavy-Hitter Oracle for Efficient Generative Inference of Large Language ModelsarXiv:2306.14048論文ページ·PDF
SnapKV: LLM Knows What You are Looking for Before GenerationarXiv:2404.14469論文ページ·PDF
Efficient Streaming Language Models with Attention SinksarXiv:2309.17453論文ページ·PDF
GQA: Training Generalized Multi-Query Transformer Models from Multi-Head CheckpointsarXiv:2305.13245論文ページ·PDF
「文脈を伸ばしたら、同時に捌ける人数が減った」
自前でLLMを動かしはじめると、だいたい最初にこれにぶつかります。モデルの重みはGPUに余裕で載っている。なのに文脈長を4Kから32Kに上げた途端、同時に処理できるリクエスト数が激減する。ひどいときは1人ぶん流しただけでメモリ不足で落ちる。
重みは1バイトも増えていないのに、です。増えたのはKVキャッシュ——過去のKeyとValueを取っておいて使い回す、あの仕組みが抱えるメモリでした。詳しくはKVキャッシュを1から理解するで扱いましたが、要点は一行です。キャッシュは、文脈長にもリクエスト数にもまっすぐ比例して膨らむ。
この記事は、それを削る側の話です。KIVI、H2O、SnapKV、GQA、アテンションシンク——名前は散らばっていますが、削れる場所は最初から3つしかなく、どの手法もそのどれかを叩いているだけです。
比喩: 会議の議事録を、全部持ち歩く
KVキャッシュは、長い会議の議事録に似ています。新しい発言をするたび、あなたはこれまでの発言全部に目を通してから話す。だから議事録は捨てられず、会議が長引くほど紙束は厚くなって机の上(GPUメモリ)を埋めていきます。
紙束を薄くする方法は、常識的に考えて3つです。細かい字で書く=量子化、要らない発言を捨てる=追い出し(eviction)、同じ内容を何人ぶんも書かない=共有。この3つが、そのままKVキャッシュ圧縮の3方向になります。比喩がここまで素直に対応するのは、次の式のせいです。
なぜKVがメモリを食うのか — 掛け算を1本
キャッシュの大きさは、推定でも理論でもなく、単なる勘定書きです。
記号を1つずつ。先頭の は K と V の2本ぶん。 は層の数(各層が自分のK・Vを持つ)。 は文脈長、つまりいま抱えているトークン数。 は KV ヘッドの数、 は1ヘッドの次元で、この2つの積が1層1トークンあたりのKのサイズです。 は1要素のバイト数(fp16なら2)。 は同時に走っているリクエスト数。
つまり「層の数だけ、トークンの数だけ、KとVを2本ずつ、ヘッドのぶん、指定の精度で、人数ぶん抱える」と言っているだけで、賢い項は1つもありません。そして重要なのは、動かせる項が3つしかないことです。 と はモデルの構造そのもので推論時には触れません。 はユーザーが送ってきた文脈、 は捌きたい人数で、減らしたら負けです。残るのが (精度)、 のうち実際に保持する数、そして (ヘッド数)。さっきの比喩の3つと、きれいに一致します。
方向1: 精度を落とす — KとVでは、落とし方が違う
いちばん素直なのは を叩くことです。fp16(2バイト)のKVをint8(1バイト)やint4(0.5バイト)で持てば、それだけで半分・4分の1になります。考え方は量子化の記事と同じで、値の範囲をスケールとゼロ点の2つの数で表し、整数の目盛りに丸めるだけです。
ただしKVキャッシュには、重みの量子化にはない事情があります。KとVで、値の散らばり方が違うのです。KIVI(2024)の観察はここを突きます。Keyのテンソルには特定のチャネル(次元)だけが極端に大きいという偏りがあり、しかもトークンが変わっても同じチャネルに現れる。一方Valueにはそうした一貫した偏りが見られません。
この非対称性が、量子化の「まとめ方」を決めます。丸めの誤差は、スケールを共有するグループに外れ値が1つ混じるだけで全体へ広がる。だからKはチャネル方向でまとめ(外れ値チャネルを他と混ぜない)、Vはトークン方向でまとめる。KIVIはこの非対称な組み方で、2ビットという攻めた精度を追加学習なしに成立させました。手法名の「asymmetric(非対称)」はここを指しています。なお実装では、直近の数十〜数百トークンだけfp16のまま残す残差窓がほぼ必ず併用されます。
方向2: 捨てる — 注意はそもそも偏っている
次は、保持するトークンの数そのものを減らす方向です。ただし「昔のものから順に捨てる」だけでは足りません。なぜ捨てても成立するのかに、根拠があります。
自己注意の重みは softmax を通ります。softmax は入力の差を指数関数で増幅するので、スコアが少し高いだけの候補に重みが集中し、残りはほぼゼロになる。実際のモデルの注意行列を見ると、ごく一部のトークンが注意の質量の大半を集めていることが分かります。
H2O(Heavy-Hitter Oracle, 2023)はこの偏りを直接使います。各トークンが今までに受け取った注意重みを累積しておき、上位の「ヘビーヒッター」と直近のトークンだけを残して残りを追い出す。キャッシュの上限を決めておけば、文脈が伸びてもメモリは一定に保てます。
SnapKV(2024)は狙う場面が違います。H2Oが生成しながら削るのに対し、SnapKVはプロンプトを読み終えた時点で一気に削る。末尾の数十トークン(観測窓)が前半のどこを見ているかを集計し、票の多かった位置だけを残す。さらに隣り合う位置がまとまって選ばれるようプーリングをかけ、文の断片がぶつ切りにならないようにします。長いプロンプトを何度も投げる用途で効きます。
どちらにも共通するのは、捨てたKVは二度と戻らないことです。粗くなるだけの量子化と違って追い出しは不可逆——運用上かなり重い違いです。
落とし穴: 先頭のトークンを捨ててはいけない
追い出しを自分で実装しようとすると、多くの人が同じ罠を踏みます。「直近N個だけ残せばいいだろう」とスライディング窓を組むと、文が窓の長さを超えた瞬間に出力が崩壊するのです。
原因は アテンションシンク(attention sink)と呼ばれる現象です。StreamingLLM の論文(2023)によれば、学習済みのTransformerは系列の先頭にある数トークンに、意味とは無関係に大量の注意を注ぐ癖を持っています。softmax は重みの合計を必ず1にするので、「どこも見なくてよい」ときにも重みをどこかへ流す必要がある。その捨て場所として、全トークンから見える先頭が使われている、という理解です。
だからスライディング窓が先頭を追い出すと、行き場を失った注意が意味のあるトークンへ押し付けられ、分布が壊れます。対処は拍子抜けするほど単純で、先頭の数トークン(論文では4つ程度で足りるとされます)を常に残し、残りを窓で回す。この「シンク+窓」の構成は、いまや長文推論の定番です。
教訓は一般化できます。追い出しポリシーは、モデルが学習時に身につけた癖と衝突しうる。 均等に見えるルールが、いちばん壊しやすい。
方向3: 共有する — ヘッドを減らす
最後の項、 です。これだけは推論時の工夫ではなく、モデルの設計側の話になります。
素朴なマルチヘッド注意は、Queryのヘッドが32個あればKとVのヘッドも32個持ちます。「Kの名簿は全ヘッドで1冊を共有すればいいのでは」としたのが MQA、極端すぎて品質が落ちるので「8個くらいのグループに分けて共有する」と間を取ったのが GQA(2023)です。 が32から8になればキャッシュはそのまま4分の1。式の項を直接叩いているので、効き方に不確実性がありません。
さらに進めてKとVを低次元の潜在ベクトルに落として保存する設計(DeepSeek-V2 の Multi-head Latent Attention など)もあります。この系統はAttentionの変種図鑑で整理しました。自分でモデルを訓練しないなら、この方向は選ぶことしかできません。逆に言えば、モデル選定時に を見ておくと、後から量子化や追い出しで苦労する量が変わります。
コードで見る: シンク+窓の最小実装
追い出しの実体は、キャッシュの添字操作でしかありません。
def evict(cache_k, cache_v, n_sink=4, n_window=1020):
# 先頭 n_sink 個は常に残す(アテンションシンク)
if cache_k.shape[0] <= n_sink + n_window:
return cache_k, cache_v
keep = list(range(n_sink)) + list(range(cache_k.shape[0] - n_window, cache_k.shape[0]))
return cache_k[keep], cache_v[keep]
n_sink を 0 にすると、この関数はただのスライディング窓になります。その1行の違いが、長文で出力が壊れるかどうかを分けます。 なお、残した位置の番号を詰め直すか元のままにするかで結果が変わる点には注意してください。
現場ではこう使う
KVキャッシュ圧縮が仕事になるのは、LLMを自前でサービングする側です。GPUの必要枚数を見積もる人、同時接続数のSLOを引く人、「長い文書を投げると落ちる」という苦情を受ける人。
まず触るのは、精度と量です。 vLLM なら --kv-cache-dtype fp8(KVだけ低精度にする)、--max-model-len(文脈長の上限)、--max-num-seqs(同時実行数)、--enable-prefix-caching(共通プレフィックスの使い回し)。順番も決まっていて、まず を落とし、次に の小さいモデルへ乗り換え、追い出しは最後。前2つは影響が読めますが、追い出しは入力によって壊れ方が変わります。
評価は、圧縮に一番弱い課題でやる。 短い応答のperplexityやチャットの見た目は、追い出しをかけてもほとんど劣化しません。壊れるのは長い文脈から特定の1点を拾うタスク——長文書へのQA、コードベースからの参照、needle系の検索試験です。「普段のチャットで問題ないので入れました」が本番の長文リクエストで崩れる、というのがいちばん多い事故です。
不可逆かどうかを区別する。 量子化は粗くなるだけ、追い出しは消えます。マルチターンで数ターン前の固有名詞を参照する用途なら追い出し系は慎重に。1回きりの要約なら、SnapKVのようなprefill時圧縮と相性が良い。
設計レビューで問われるのは「文脈長を2倍にすると何が2倍になるか」です。 KVメモリは線形に2倍、prefillの注意計算は2乗で4倍。加えて「シンクを残さない窓はなぜ壊れるか」を説明できるかどうかで、理解の深さが分かれます。
まとめ
- キャッシュ量は 。推論時に動かせるのは ・保持トークン数・ の3つだけ
- 精度を落とす: KIVIは、Keyに外れ値チャネルがありValueには無いという非対称性を使い、Kはチャネル方向・Vはトークン方向でまとめる
- 捨てる: H2Oは累積注意でヘビーヒッターを残し、SnapKVはプロンプト末尾の投票でprefill時に一括で削る
- 先頭は捨ててはいけない: モデルは先頭数トークンを注意の捨て場(アテンションシンク)に使っている。シンク+窓が定番
- 共有する: GQAは を直接減らす。推論時ではなくモデル選定時の判断
- 量子化は粗くなるだけ、追い出しは不可逆。評価は長文検索系の課題で行う
コメント
コメントにはログインが必要です