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?

図でわかる Transformer と KV キャッシュ(LLM が長文脈で重くなる理由)

0
Last updated at Posted at 2026-10-13

はじめに

LLM のニュースでよく見る「KV キャッシュ」「prefill / decode」を、Transformer の仕組みから整理するために書きました。対象は、Transformer の中身を小さな数値の例で一度つかみたい人です。
結論: LLM は1トークンずつ文章を作り、そのたびに過去のトークンの Key・Value を作り直さず保存して使い回します(KV キャッシュ)。計算は減りますがメモリを大量に使い、これが長文脈や同時接続で LLM が重くなる主な理由です。

1. LLM がやっていることは「次の1語の予測」だけ

next-token.txt
入力:  "今日は 天気 が"
          |
     [ Transformer ]
          |
出力:  次の語の確率   "良い" 62% / "悪い" 20% / ...
          |
     "良い" を選んで入力の末尾に足す → もう一度 → ...

文章は「1トークン予測 → 足す」の繰り返しで作られます(確率の数値は説明用の例です)。

2. Transformer の中身(全体図)

transformer-overview.txt
 "今日" "は" "天気" "が"        ← トークン(語のかけら)
   |     |     |     |
 [ 埋め込み: 各トークンを数値ベクトルに ]
   |     |     |     |
 +--------------------------+
 | Attention: 他の語を見る   |  ┐
 |--------------------------|  │ 1つの「層」
 | FFN: 各語を個別に加工     |  ┘
 +--------------------------+
        × 32〜100層 くり返す
   |
 [ 最後の語のベクトル → 次の語の確率 ]
  • Attention: 「この語を理解するには、文中のどの語が関係あるか」を見て情報を混ぜる(語どうしのやりとり)
  • FFN: 混ぜた結果を1語ずつ加工する(知識の多くはここに入っているとされる)
  • これを何十層も重ねる。浅い層ほど文法的な情報、深い層ほど意味・文脈的な情報を扱う傾向があると言われる(はっきり分かれているわけではない)

2-1. 埋め込み: 語を「数値の並び(ベクトル)」にする

コンピュータは文字のままでは計算できないので、各トークンを数値の並びに置き換えます。どの語をどの数値にするかは、学習で決まった対応表で引きます。

embedding-example.txt
 "猫"   → [ 0.9, 0.1, 0.3, ... ]
 "魚"   → [ 0.2, 0.8, 0.4, ... ]
 "食べた" → [ 0.5, 0.5, 0.9, ... ]
          ↑ 実際は数千個の数値が並ぶ(数値は説明用の例)

学習の結果、一般的な傾向として、意味が近い語ほど似た数値の並びになります。以降の「猫の V」なども、すべてこのような数値の並びのことです。

2-2. 「情報を混ぜる」とは

ある語のベクトルに、関係のある他の語の情報を、関係の強さに応じた重みを付けて足し込むことです。

mixing-example.txt
 「猫 が 魚 を 食べた。それ は 満足 した」の "それ"

  処理前の "それ" : 「何かを指す語」という情報しかない
                      |
       文中を見て、"猫" との関係が強いと判断
                      |
  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段階です。

attention-flow.txt
 例:「猫 が 魚 を 食べた」の "食べた" を処理するとき(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 を作ります。

qkv-projection.txt
                              ┌─ × 重み W_Q ─→ Q(質問)
 層に入ってきた "食べた" の x ─┼─ × 重み W_K ─→ K(見出し)
                              └─ × 重み W_V ─→ V(中身)

「重みを掛ける」の中身は、入力の各数値に決まった係数を掛けて足すことです。数値が2個の小さな例で書くと次のとおりです。

q-example.txt
 層に入ってきた "食べた" のベクトル 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 を掛けて作ったものです。

score-example.txt
 "食べた" の 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つです。

  1. スコアを √d_k で割る。d_k は K の数値の個数で、この例では2なので √2 ≒ 1.414 で割ります。数値の個数が多いほどスコアが大きくなりやすく、そのままだと割合が1語に偏りすぎるためです
  2. 各値の exp を計算する。exp(x) は e(約2.718)の x 乗で、どんな値も正の数になり、元の値が大きいほど大きくなります
  3. exp の値を、その合計で割る

2 と 3 をまとめて softmax と呼びます。

softmax-example.txt
          スコア   ÷√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個目を「魚らしさ」と見なした説明用の値。実際の数値に、このような名前は付いていません)。

weighted-sum-example.txt
         割合      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 を並行して計算します。そのあと次の流れで "食べた" のベクトルが更新されます。

  1. 全ヘッドの出力をつなげ(結合)、出力用の重み W_O を掛ける
  2. その結果を、層に入ってきた元のベクトル x に足す(残差接続。正規化も挟む)
  3. FFN でも同じように加工した結果を足し、次の層の入力にする

元のベクトルに足すので、"食べた" 自身の情報は残ったまま、周りの語の情報が加わります。

4. ここで KV キャッシュが出てくる

kv-cache.txt
 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 キャッシュのサイズはおおよそ次のとおりです。

kv-cache-size.txt
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 キャッシュをリクエストをまたいで使い回すものです。先頭が完全に一致した部分だけ再利用できるので、「システムプロンプトなど固定の部分を先頭に置く」のがコツになります。

参考

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?