はじめに
LLM のニュースでよく見る「KV キャッシュ」「prefill / decode」を、Transformer の仕組みから整理するために書きました。対象は、Transformer の中身を小さな数値の例で一度つかみたい人です。
結論: LLM は1トークンずつ文章を作り、そのたびに過去のトークンの Key・Value を作り直さず保存して使い回します(KV キャッシュ)。計算は減りますがメモリを大量に使い、これが長文脈や同時接続で LLM が重くなる主な理由です。
1. LLM がやっていることは「次の1語の予測」だけ
入力: "今日は 天気 が"
|
[ Transformer ]
|
出力: 次の語の確率 "良い" 62% / "悪い" 20% / ...
|
"良い" を選んで入力の末尾に足す → もう一度 → ...
文章は「1トークン予測 → 足す」の繰り返しで作られます(確率の数値は説明用の例です)。
2. Transformer の中身(全体図)
"今日" "は" "天気" "が" ← トークン(語のかけら)
| | | |
[ 埋め込み: 各トークンを数値ベクトルに ]
| | | |
+--------------------------+
| Attention: 他の語を見る | ┐
|--------------------------| │ 1つの「層」
| FFN: 各語を個別に加工 | ┘
+--------------------------+
× 32〜100層 くり返す
|
[ 最後の語のベクトル → 次の語の確率 ]
- Attention: 「この語を理解するには、文中のどの語が関係あるか」を見て情報を混ぜる(語どうしのやりとり)
- FFN: 混ぜた結果を1語ずつ加工する(知識の多くはここに入っているとされる)
- これを何十層も重ねる。浅い層ほど文法的な情報、深い層ほど意味・文脈的な情報を扱う傾向があると言われる(はっきり分かれているわけではない)
2-1. 埋め込み: 語を「数値の並び(ベクトル)」にする
コンピュータは文字のままでは計算できないので、各トークンを数値の並びに置き換えます。どの語をどの数値にするかは、学習で決まった対応表で引きます。
"猫" → [ 0.9, 0.1, 0.3, ... ]
"魚" → [ 0.2, 0.8, 0.4, ... ]
"食べた" → [ 0.5, 0.5, 0.9, ... ]
↑ 実際は数千個の数値が並ぶ(数値は説明用の例)
学習の結果、一般的な傾向として、意味が近い語ほど似た数値の並びになります。以降の「猫の V」なども、すべてこのような数値の並びのことです。
2-2. 「情報を混ぜる」とは
ある語のベクトルに、関係のある他の語の情報を、関係の強さに応じた重みを付けて足し込むことです。
「猫 が 魚 を 食べた。それ は 満足 した」の "それ"
処理前の "それ" : 「何かを指す語」という情報しかない
|
文中を見て、"猫" との関係が強いと判断
|
Attention の出力 = "猫" の情報を多めに、他の語を少し、重みを付けて足したもの
|
処理後の "それ" = 処理前の "それ" + Attention の出力(を変換したもの)
→ 「"猫" を指している」ことを表す情報が加わる(説明用の例)
語は単独だと意味があいまいです(「それ」が何か、「食べた」の主語は誰か)。周りの語の情報を取り込むことで、文脈に合わせたベクトルに更新されます。重みの決め方が次の章の Q・K・V で、元のベクトルに足すまでの流れは 3-5 で説明します。
3. Attention は文中での「検索」
各トークンが3つのベクトルを作ります。
- Query (Q): 自分が探しているもの(質問)
- Key (K): 自分は何についての語か(見出し)
- Value (V): 自分が渡す中身(本文)
図書館の検索にたとえると、Q が検索ワード、K が各本のタイトル、V が本の中身です。Q と K がよく合う語ほど、その語の V を多く受け取ります。1つの Attention(ヘッド)の計算は、次の3段階です。
例:「猫 が 魚 を 食べた」の "食べた" を処理するとき(1つのヘッド)
"食べた" の Q
| ① Q と各語の K の内積でスコアを出す(3-2)
v
5語のスコア
| ② √d_k で割り、softmax で合計1の割合にする(3-3)
v
5語の割合
| ③ 割合を重みにして、各語の V を足す(3-4)
v
このヘッドの出力
GPT 系の LLM は「自分自身と、自分より前の語」だけを見ます(未来の語は答えなので見せない)。"食べた" は最後の語なので、自分を含む5語すべてを見ます。以降の 3-1〜3-4 は、同じ1つの例を通しで計算します(数値は説明用の値です)。
3-1. Q・K・V の作り方
Q・K・V は、その層に入ってきたトークンのベクトルに、3種類の「重み」(学習で決まった数値の表)をそれぞれ掛けて作ります。1層目に入るのは 2-1 の埋め込み(語の位置の情報も加わる)で、2層目以降は前の層で更新されたベクトルです。同じ語でも、層ごとに違うベクトルから Q・K・V を作ります。
┌─ × 重み W_Q ─→ Q(質問)
層に入ってきた "食べた" の x ─┼─ × 重み W_K ─→ K(見出し)
└─ × 重み W_V ─→ V(中身)
「重みを掛ける」の中身は、入力の各数値に決まった係数を掛けて足すことです。数値が2個の小さな例で書くと次のとおりです。
層に入ってきた "食べた" のベクトル x = [1.0, 0.5]
Q の1個目 = 0.8×1.0 + 0.4×0.5 = 1.0 ← 0.8, 0.4 などが W_Q の中の係数
Q の2個目 = 0.3×1.0 + 1.0×0.5 = 0.8
→ "食べた" の Q = [1.0, 0.8]
K・V も、係数の違う表(W_K・W_V)で同じように計算します。係数は人が決めるのではなく、大量の文章で「次の語の予測」が当たるように学習して決まります。実際は数千個の数値を扱いますが、やっていることは「掛けて足す」の繰り返しです。
3-2. スコア: Q と K の内積
"食べた" の Q と各語の K で、同じ位置の数値どうしを掛けて足します(内積)。各語の K も、3-1 と同じように W_K を掛けて作ったものです。
"食べた" の Q = [1.0, 0.8]
猫 の K = [0.9, 0.1] → 1.0×0.9 + 0.8×0.1 = 0.98
が の K = [0.1, 0.0] → 1.0×0.1 + 0.8×0.0 = 0.10
魚 の K = [0.2, 0.9] → 1.0×0.2 + 0.8×0.9 = 0.92
を の K = [0.0, 0.1] → 1.0×0.0 + 0.8×0.1 = 0.08
食べた の K = [0.4, 0.3] → 1.0×0.4 + 0.8×0.3 = 0.64
内積は、2つのベクトルの向きが似ているほど大きくなります。ただし数値が大きい(ベクトルが長い)ほど大きくもなるので、「向きの近さ」と「大きさ」の両方を反映した値です。
3-3. 割合に直す: √d_k で割って softmax
スコアを、合計が1になる割合に直します。手順は3つです。
- スコアを √d_k で割る。d_k は K の数値の個数で、この例では2なので √2 ≒ 1.414 で割ります。数値の個数が多いほどスコアが大きくなりやすく、そのままだと割合が1語に偏りすぎるためです
- 各値の exp を計算する。exp(x) は e(約2.718)の x 乗で、どんな値も正の数になり、元の値が大きいほど大きくなります
- exp の値を、その合計で割る
2 と 3 をまとめて softmax と呼びます。
スコア ÷√2 exp ÷合計(7.620)
猫 0.98 0.6930 1.9996 0.262
が 0.10 0.0707 1.0733 0.141
魚 0.92 0.6505 1.9166 0.252
を 0.08 0.0566 1.0582 0.139
食べた 0.64 0.4525 1.5723 0.206
---------------------------------------------
合計 7.6200 1.000
猫と魚の割合が大きく、「が」「を」は小さくなりました。この例では、√2 で割ったスコアが約0.057〜0.693と近いので、割合の差も小さめです。softmax の偏りは割ったあとのスコアの差で決まるため、2次元でも Q・K の値によっては強く偏ります。
3-4. 割合を重みにして V を足す
各語の V(数値の並び)に 3-3 の割合を掛け、同じ位置どうしを足します。V を2個の数値にした例です(1個目を「猫らしさ」、2個目を「魚らしさ」と見なした説明用の値。実際の数値に、このような名前は付いていません)。
割合 V 割合×V
猫 0.262 × [1.0, 0.0] = [0.2620, 0.0000]
が 0.141 × [0.1, 0.1] = [0.0141, 0.0141]
魚 0.252 × [0.0, 1.0] = [0.0000, 0.2520]
を 0.139 × [0.1, 0.1] = [0.0139, 0.0139]
食べた 0.206 × [0.5, 0.5] = [0.1030, 0.1030]
------------------------------------------------ 位置ごとに足す
合計 = [0.393, 0.383]
結果は、猫の V と魚の V の情報を両方多めに取り込んだベクトルです。これが2章の「情報を混ぜる」の中身です。ただし、これは説明用の例で、この2つの数値だけで「猫が主語・魚が目的語」と判定できるわけではありません。
3-5. ヘッドの出力から、語のベクトルの更新まで
3-4 の結果は、1つの Attention ヘッドの出力です。実際の層は W_Q・W_K・W_V の組を複数持ち(マルチヘッド)、ヘッドごとに 3-1〜3-4 を並行して計算します。そのあと次の流れで "食べた" のベクトルが更新されます。
- 全ヘッドの出力をつなげ(結合)、出力用の重み W_O を掛ける
- その結果を、層に入ってきた元のベクトル x に足す(残差接続。正規化も挟む)
- FFN でも同じように加工した結果を足し、次の層の入力にする
元のベクトルに足すので、"食べた" 自身の情報は残ったまま、周りの語の情報が加わります。
4. ここで KV キャッシュが出てくる
1語目生成: [今日][は][天気][が] → 4語ぶん K,V を計算
2語目生成: [今日][は][天気][が][良い] → 前の4語の K,V は
さっきと同じ値!
キャッシュなし: 毎回 全語の K,V を計算し直す
キャッシュあり: 新しい1語の K,V だけ計算し、保存分に足す
保存(KVキャッシュ) 新規
+----+----+----+----+ +------+
|今日| は |天気| が | + | 良い | → 新語の Q で全部を検索
+----+----+----+----+ +------+
- 過去の語の K,V は変わらないので取っておける → 速い
- キャッシュで省けるのは「過去の語の K,V(とそこに至る各層の計算)の作り直し」。キャッシュなしだと n 語目のたびに n 語ぶんを全層で計算し直すので、K,V を作る計算だけでも全体で約 n² になる。ありなら毎回1語ぶんで済む
- ただし Attention そのもの(新語の Q と過去の全 K の比較)は省けない。1語ごとに文脈の長さに比例した計算が残るので、長文脈ほど1語あたりの生成は遅くなる
たとえると会議の議事録係です。新しい発言のたびに録音を最初から聞き直す(キャッシュなし)か、要点メモを足していき、メモだけ見返す(キャッシュあり)か、の違いです。
処理の2段階
- prefill: プロンプト全体を一気に計算し、全トークンの K,V をキャッシュに入れる。prefill は TTFT(最初の出力トークンが届くまでの時間)の主な要因です。TTFT には待ち行列や通信などの時間も含みます
- decode: 1トークン生成するたびに、その1トークン分の K,V だけ計算してキャッシュに足す
5. 代償はメモリ
KV キャッシュのサイズはおおよそ次のとおりです。
2(K と V)× 層数 × KVヘッド数 × ヘッドの次元 × 1要素のバイト数 × トークン数 × 同時リクエスト数
例: Llama 3.1 8B(32層、KVヘッド8、次元128。最大128Kトークン)を、FP16(2バイト)・同時リクエスト1件で使う場合
- 1トークンあたり 2 × 32 × 8 × 128 × 2 = 131,072 バイト = 128 KiB
- 8K トークンで 1 GiB、128K トークンで 16 GiB(FP16 のモデル本体 約16GB と同程度)
- 同時リクエストが10件なら、その10倍になる
長文脈・同時接続が増えると、GPU のメモリの大半が KV キャッシュになります。「文脈を長くすると遅く・重くなる」の主な原因です。
6. KV キャッシュを減らす工夫(ニュースで見かける用語)
- GQA / MQA: 複数の Query ヘッドで K,V を共有し、KV ヘッド数を減らす(上の例の「8」がこれ)
- KV キャッシュの量子化: FP16 → FP8 / INT4 で保存する
- PagedAttention(vLLM): OS のページングのように細切れに確保し、無駄な空きを減らす
- Sliding window / 古いトークンの破棄: 直近の分だけ残す
- MLA(DeepSeek): K,V を小さく圧縮して保存する
7. Prompt caching との関係
Claude や OpenAI の API の Prompt caching は、この KV キャッシュをリクエストをまたいで使い回すものです。先頭が完全に一致した部分だけ再利用できるので、「システムプロンプトなど固定の部分を先頭に置く」のがコツになります。
参考
- Vaswani et al.「Attention Is All You Need」(Transformer の原論文。3.2 節に Attention の式): https://arxiv.org/abs/1706.03762
- Jay Alammar「The Illustrated Transformer」(定番の図解): https://jalammar.github.io/illustrated-transformer/
- 3Blue1Brown「Attention in transformers」(動画): https://www.youtube.com/watch?v=eMlx5fFNoYc
- Transformer Explainer(ブラウザで GPT-2 の中を動かして見られる): https://poloclub.github.io/transformer-explainer/
- Hugging Face「KV Caching Explained」: https://huggingface.co/blog/not-lain/kv-caching
- vLLM の PagedAttention の論文: https://arxiv.org/abs/2309.06180