LLM推論のボトルネックはほぼ KVキャッシュ に集約される。 この教材では前半で、KVキャッシュが何を貯めていて、なぜ膨らみ、なぜ帯域を食い潰すのかを 電卓で手を動かしながら確認する。後半は、その膨らみを畳むために持ち出される 「回帰」という言葉が、実は3つの別物を指していることを分解する—— 問題を作っている回帰、解いている回帰、それを成立させる条件としての回帰。
Attention は、各トークンの隠れ状態 h から3本のベクトルを作る。
自分の q は1本、突き合わせる相手は 過去全部の K と V。 そしてここが要点で、因果マスクがあるため位置 t の k, v は それより後の情報に一切依存しない。つまり一度計算したら二度と変わらない。 ならば捨てずに取っておけばいい——これが KVキャッシュ。
各ステップで使うクエリはいま生成している1トークンぶんだけで、使ったら用済み。 過去のクエリを取っておいても二度と参照しない。だから「QKVキャッシュ」ではなく KVキャッシュと呼ぶ。名前が非対称なのは、参照のされ方が非対称だから。
| 1ステップの計算 | Tトークン生成の総量 | 代償 | |
|---|---|---|---|
| キャッシュなし | 過去全部の K,V を作り直す | 再計算が積み上がる | 計算が爆発 |
| キャッシュあり | 新しい1トークンぶんだけ | attention 本来のコストのみ | メモリが膨らむ |
KVキャッシュは計算をメモリに両替する装置である。そして現在の LLM 推論では、 この両替レートが悪いほうへ振れきっている。どれくらい悪いのかを、次の電卓で見る。
数式は単純だ。単純なのに、出てくる数字がGPUの使い方を決めてしまう。
MHA 7B級は 512 KiB/token、GQA 70B級は 320 KiB/token。 パラメータが10倍でも、KVキャッシュはむしろ小さい。 GQA(Grouped-Query Attention)が KVヘッドを 32→8 に削っているからで、 モデルの大きさと KVキャッシュの大きさは別の量だと分かる。 ここを混同していると、長文でメモリが死ぬ理由を永久に取り違える。
decode の1ステップで GPU がやることを分解する。
現代のGPUは、演算器の性能に対してメモリ帯域が相対的に細い。ざっくり 1バイト読むごとに300FLOPsほど計算しないと演算器が遊ぶ。 ところが decode は1トークンあたりの計算がごく小さいので、完全に読み出し待ちになる。 これが memory-bound の正体で、GPUを速い演算器に替えても効かない領域である。
モデル重みはバッチ全体で共有できる。KVキャッシュはリクエストごとに固有で、共有できない。
だからバッチを増やすと、重みの読み出しコストは頭数で割られて薄まっていく。
ところが KVキャッシュのトラフィックだけは本数に比例して増え続ける。
電卓で同時本数のスライダーを動かすと、演算強度が一度上がってから頭打ちになるのが見えるはずだ。
頭打ちを作っている項が KVキャッシュで、マルチクエリ配信で最後に残る律速項がこれ。
効率指標として「速度」ではなく TPM=メモリあたりスループット が使われるのは、
分母のほうが効くこういう構造だからである。
ここは電卓が答えてくれない部分なので、原理として押さえておきたい。
上の電卓が出しているのはある瞬間のキャッシュのサイズで、これは文脈長 T に比例して伸びる。
しかし本当に効いてくるのは、T トークンを生成しきるまでに読み出す総量のほうである。
1トークン目は 1 個ぶん、2トークン目は 2 個ぶん…… t トークン目は t 個ぶんの状態を読む。足し上げると、
文脈が2倍になれば、占有は2倍だが、読み出し総量は4倍になる。
「長文にすると急に重くなる」という体感の正体はこの二乗の項で、
メモリ律速の議論で本当に効いているのはサイズではなく帯域を流れる総量のほうだ。
だから対策も2種類ある——サイズを削る(量子化、ヘッド削減)のと、
読み出しの回数・範囲を削る(window、階層化)のは、同じ「KV対策」でも効き方が違う。
前者は係数を下げるだけで二乗という形は変わらない。後者だけが形を変えられる。
| 層 | 手法 | 式のどこを削るか | 既存モデルに後付け |
|---|---|---|---|
| ヘッド構成 | MQA / GQA / MLA | KVヘッド数(幅) | ✕ 事前学習が要る |
| 数値表現 | KV量子化(fp8 / int4) | バイト数 | ○ |
| 注意範囲 | Sliding window / Sparse | 系列長(見る範囲を切る) | △ |
| 取捨選択 | eviction(H2O, StreamingLLM 等) | 系列長(重要でないものを捨てる) | ○ |
| メモリ管理 | PagedAttention(vLLM) | 断片化を直す(量は減らない) | ○ |
| 系列そのもの | 階層化(PHOTON 等) | キャッシュが張られる列の解像度 | ✕ 事前学習が要る |
上から5つは「できあがったキャッシュをどう安くするか」という後付けの工夫。 最後の一行だけが、そもそもキャッシュが小さくなる形にモデルを学習し直すという別の階層の話になっている。 強みでもあり、既存の重みに適用できないという障壁でもある。
論文でも記事でも同じ字が3つの別物に使われる。混ぜると読めなくなるので分ける。カードを押すと図が変わる。
2つの計算経路が同じ結果に到達すると証明できるなら、実行時は安いほうを選んでよい。
高い経路(生成したトークンを上げ直して要約を作る)と、安い経路(デコーダの途中に出ている再構成をそのまま使う)。
両者の一致を訓練時に損失として作り込んでおいて、推論時に安いほうへ差し替える。
近似による手抜きではなく、等式の利用である。この型は KVキャッシュに限らず応用が効く。
「履歴を状態に畳む(回帰)」と「履歴を全部残して毎回引く(注意)」は、メモリと表現力の交換レートを決める根本の選択。
RNN は履歴を固定サイズの状態に畳むのでメモリは O(1)。そのかわり 「300トークン前のあの単語」に戻って参照することができない。 Transformer は全部残すのでどこへでも参照できるが、メモリは O(T)。 階層化は、この線分の上に目盛りを1本打つ試みだと読める—— 粗い粒度でならランダムアクセスできる状態を O(T/C) で持つ。
固定サイズの状態にすべてを押し込む方式には、構造的な性質がひとつある。
新しい情報が入るたびに状態は上書き・混合されていくので、
古い情報ほど薄まっていく。容量が一定である以上これは避けられず、
「1万トークン前に一度だけ出てきた固有名詞」を取り出せる保証は原理的にない。
階層化が違うのは、畳むが、畳んだものを捨てない点にある。
粗い要約の列そのものを保持しているので、
粗い粒度でならいつでも過去へ戻れる。
失われるのは「遠い過去の細部」であって「遠い過去そのもの」ではない——
圧縮率 C が、何を失ってよいかのダイヤルになっている。
前節で見たとおり、読み出し総量は O(T²) だった。 グローバルに保持する列が 1/C に縮むと、この項は
へ置き換わる。C=16 なら二乗の項が 256分の1。 量子化や KVヘッド削減が係数を下げる手であるのに対し、 これは二乗が乗る対象そのものを短くする手である。 効き方の質が違い、しかも長くなるほど差が開く。 「長文と多並列でこそ効く」と言われるのは、この形の違いに由来している。
自分の出力を自分に食わせ続ける構造である以上、ズレがあれば累積する。 教師強制(正解を与えながら)で訓練したモデルを自由生成させると壊れていく、 いわゆる exposure bias と同じ形の問題が、 再帰的な生成手続きにはそのまま乗ってくる。
再帰的な生成はモデル自身が作った再構成を次の入力にするので、 正解トークンを与えながら測る標準的な teacher-forcing の評価パイプラインに素直に乗らない。 結果として「効率は新しい手続きで測り、品質は従来の手続きで測る」という 非対称が生じやすい。数字を引用するときは、どちらの手続きで測られた数字かを 確認する癖をつけておくといい——実際、PHOTON の論文はこの点を自ら限界として明記している。
第一部の言葉に戻すと、KVキャッシュという「読み直し」を「持ち回る状態」に両替した以上、 両替レートは生成が長くなるほど悪化しうる。 回帰にした瞬間に引き受ける、構造的なリスクである。
ここで見た KVキャッシュの膨らみを「列の解像度を粗くする」ことで畳みにいった実例。 本教材の ②再帰的生成 と ③再帰整合性 が、そのまま RecGen という具体的な仕掛けとして出てきます。 報道の「475倍」を スループット×メモリ に分解し、どこまでが階層化一般の効果でどこからが固有の貢献かを切り分けた記事。
こちらはパラメータ側の非対称の話。「MoE は軽い」の軽さが計算だけで、 メモリは軽くないのはなぜか。本教材の「共有できるもの/できないもの」の議論と対になります。