TT Lab
ブログ

한국어 English 日本語

はじめる
ブログ

ブログ

KVキャッシュはなぜ必要なのか:再計算を避けるための記憶

ステータス:ドラフト(2026-10-13)、一部実測。 おもちゃモデルの測定(第3節)は筆者がノートPCのCPU上で直接計測した値です。GPUサーバーの測定(第4・6節)は、同一サイトの「トスML/AI Infra準備」シリーズにて8GB級GPU1枚で計測された値を引用しています。モデル構成は公式のconfigと論文の表で確認し(2026-10-13閲覧)、KVサイズはその値に基づいて計算しました。 作成ルール:実測データを基本とし、計測できなかったものは「未実測」、確認できなかったものは「未確認」と表記します。

目次

1. トークンを1つずつ生成するモデル

チャットボットの返答は一度には出力されません。モデルはこれまでのテキスト(質問 + これまでに生成した返答)を見て 次のトークン1つを選択し、そのトークンをテキストの末尾に追加してさらに次のトークンを選択します。これを自己回帰デコーディングと呼びます。

トークンを1つ選択する際、トランスフォーマーの各層はアテンションを計算します。トークンごとに3つのベクトルが作成されます。

新しいトークンのQを過去のトークンのKと1つずつ比較してスコアを算出し(どれほど関連があるか)、そのスコアでVを重み付けして混ぜ合わせます。数式で書くと softmax(q·Kᵀ/√d)·V となります。

ここで重要な性質が1つあります。言語モデルのアテンションには 未来を見ないようにマスクがかけられており(因果マスク / causal mask)、3番目のトークンのK・Vは1〜3番目のトークンのみを見て作成されるため、後ろにトークンが100個追加されても 変化しません。 であれば、一度作成したK・Vを破棄して再計算する理由はありません。保存しておき、次のステップで取り出して使えばよいのです。これが KVキャッシュです。

Qは保存しません。過去のトークンのQは、そのトークンを生成する際に1度使用したら二度と使うことがないためです。新しいステップで必要なQは、新しいトークンの分が1つあるのみです。

2. キャッシュがない場合、ステップごとに最初から

토큰 5개를 만드는 동안 계산하는 K·V 캐시 없음 — 걸음마다 처음부터 다시 걸음 1 걸음 2 걸음 3 걸음 4 걸음 5 앞 토큰의 K·V 를 버리고 매번 전부 다시 만든다 계산 1+2+3+4+5 = 15칸 n 걸음이면 약 n²/2 캐시 있음 — 새 토큰만 계산, 지난 것은 읽기 걸음 1 걸음 2 걸음 3 걸음 4 걸음 5 새 토큰의 K·V 하나만 만들어 캐시에 붙이고, 어텐션은 전부 본다 계산 5칸, 읽기 10칸 n 걸음이면 계산 n칸 새로 계산한 K·V 캐시에서 읽은 K·V 한 칸 = 한 토큰의 K·V(모든 층). 어텐션 점수 계산은 두 방식 모두 지난 토큰 수만큼 든다.
図:キャッシュなしとありのデコーディング比較

キャッシュがない場合、t番目のステップでt個のトークンすべてのK・Vを再作成します。層ごとに行う線形射影(Q・K・V・出力)とMLPはトークン数に比例するため、1ステップのコストはtに比例し、nステップを合計すると1+2+…+n ≈ n²/2 となります。キャッシュがある場合、ステップごとに新しいトークン1つのみを射影してMLPに入力するため、nステップでは n になります。

アテンションスコアの計算は、どちらの方式でも残ります。キャッシュがあっても新しいトークンのQは過去すべてのKと比較する必要があるため、1ステップあたりtのコストがかかります(nステップの合計でn²/2)。キャッシュがない場合、この計算すらステップごとにt²となるため、合計がn³/3へと膨れ上がります。コンテキストが非常に長くならない限り、射影とMLPが計算の大半を占めるため、実際に体感する差は「ステップごとにt対1」となります。

3. 実験してみる:numpy製おもちゃトランスフォーマー

言葉では簡単でも体感が難しいため、重みを乱数で埋めた小さなトランスフォーマーをnumpyで実装し、両方式を測定しました。学習済みモデルではないため出力トークンに意味はありませんが、計算の形状(射影・アテンション・MLP・正規化)は本物と同じです。

キャッシュあり方式の核心は数行です。今回のトークンのK・Vをあらかじめ確保した配列の末尾に書き込み、アテンションはこれまで蓄積されたすべてを参照します(ヘッドごとに分割するreshapeは省略しました)。

k = mm(h, W["k"])  # 새 K
v = mm(h, W["v"])  # 새 V
n = cache["n"]
cache["k"][:, n:n+T] = k  # 끝에 붙임
cache["v"][:, n:n+T] = v
K = cache["k"][:, :n+T]  # 지난 것 전부
V = cache["v"][:, :n+T]
a = attend(q, K, V)

結果1. 出力は同じだが、演算量と時間は大きく異なる

演算量(GFLOP、コードによる計測値):

全体長 キャッシュなし キャッシュあり 倍率
128 6.71 0.121 55倍
256 31.0 0.264 118倍
512 148.2 0.599 247倍

時間(秒、3回の中央値):

全体長 キャッシュなし キャッシュあり 倍率
128 0.205 0.084 2.5倍
256 1.16 0.131 8.9倍
512 8.01 0.485 16.5倍
한 토큰을 만드는 데 걸린 시간(ms) numpy 장난감 모델 · 노트북 CPU · 실측(64토큰 구간 중앙값) 0 10 20 30 40 0 128 256 384 512 지금까지의 문맥 길이(토큰) 캐시 없음 35ms 캐시 있음 0.4→0.6ms 캐시 없음(실선·점) 캐시 있음(점선) 512토큰 전체: 없음 8.0초, 있음 0.49초 · 연산량 247배 차이
図:生成位置ごとの1ステップの時間

結果2. キャッシュがない場合、1ステップの時間がコンテキストに比例して遅くなる。 512トークンの実行において64トークンの区間ごとに1ステップの時間の中央値を見ると、キャッシュなしは1.25msから35.1msに増加し、キャッシュありは0.43msから0.60msへとわずかな増加にとどまりました。キャッシュがあっても少しずつ遅くなるのは、第2節で述べたアテンションスコアの計算(1ステップあたりt)が原因です。

結果3. キャッシュ配列のサイズは公式の数式とバイト単位まで一致する。 1,024トークンをプリフィルした後のキャッシュ配列の nbytes を加算しました。

クエリヘッドはいずれも8個であり、公式の値は「2 × 層数(2) × KVヘッド数 × ヘッド次元(16) × トークン数(1,024) × 4バイト」です。

方式 KVヘッド 実測バイト数 公式の値
MHA 8 2,097,152 2,097,152
GQA 2 524,288 524,288
MQA 1 262,144 262,144

3つの方式すべてにおいて、キャッシュを使用した最後のロジットは、キャッシュなしで最初から計算した値と一致しました(許容誤差10⁻³)。

4. キャッシュの代償:メモリ

토큰 하나가 남기는 KV 의 크기 2 × 층 × KV 헤드 × 헤드 차원 × 바이트 K 와 V 두 벌 · 이 값에 토큰 수를 곱하면 한 요청의 KV MHA GQA MQA MLA 헤드마다 KV 그룹마다 KV KV 하나 공유 압축해 저장 8칸 (기준) KV 헤드 = 쿼리 헤드 2칸 (1/4) 헤드 4개가 한 벌 공유 1칸 (1/8) 모든 헤드가 한 벌 잠재 벡터 하나 쓸 때 K·V 로 펼침 쿼리 헤드(저장 안 함) 캐시에 남는 K·V Llama 3.1 70B(80층·헤드 차원 128·bf16): GQA 8 → 320KiB/토큰 KV 헤드가 쿼리 헤드만큼(64) 있었다면 2.5MiB/토큰 (계산)
図:KVメモリの公式とMHA・GQA・MQA・MLA

キャッシュとは、計算量をメモリに変換したものです。トークン1つが残すKVは層ごとにKとVの1セットずつであるため、以下のようになります。

KVバイト/トークン = 2 × 層数 × KVヘッド数 × ヘッド次元 × データ型のバイト数

この数式が実際のサーバーと一致するかどうかは、別のシリーズで確認しました。Qwen3-1.7B(28層、KVヘッド8、ヘッド次元128、bf16)の数式上のトークンあたりのサイズは114,688バイトであり、vLLMサーバーログのKVキャッシュメモリ(2.36GiB)をトークン数(22,048)で割った値は約114,900バイトとなり、0.2%以内で一致しました(8GB級GPU、vLLM 0.31.0、実測)。KVを8ビット(fp8)にすると、同じメモリに41,920トークンが収まり、およそ2倍近くなりました。

よく知られた公開モデルに当てはめると以下のようになります。層数・ヘッド数は公式資料から確認し、サイズはKVをbf16(2バイト)として計算しました。以下のモデルはすべてヘッド次元が128です。「32K」は32,768トークンのリクエスト1つあたりのKVです。

モデル 層数 × KVヘッド トークンあたりKV 32K
Qwen2.5-1.5B 28 × 2 28KiB 0.88GiB
Qwen2.5-7B 28 × 4 56KiB 1.75GiB
Llama 3.1 8B 32 × 8 128KiB 4GiB
Mistral-7B 32 × 8 128KiB 4GiB
Qwen3-8B 36 × 8 144KiB 4.5GiB
Llama 3.1 70B 80 × 8 320KiB 10GiB
Llama 3.1 405B 126 × 8 504KiB 15.75GiB
70BがMHAだった場合(仮定) 80 × 64 2.5MiB 80GiB
DeepSeek-V2 (MLA) 60層、潜在576 67.5KiB 2.11GiB

クエリヘッド数は、Qwen2.5-1.5Bが12、Qwen2.5-7Bが28、Llama 3.1 8B・Mistral-7B・Qwen3-8Bが32、70Bが64、405Bが128です。KVヘッドがクエリヘッドよりも少ないのが、第5節で扱うGQAです。

5. サイズを縮小する方法:MHA → GQA → MQA → MLA

数式の中で削減できるのは、KVヘッド数(またはそれに代わるもの)とバイト数です。層数とヘッド次元はモデルの能力に直結するため、安易に減らすことはできません。

なぜここまでして減らそうとするのでしょうか。第6節がその答えです。

6. プリフィルとデコード:性格が異なる2つの段階

1つのリクエストは2つの段階を経ます。

  1. プリフィル(prefill): プロンプト全体を一度に入力してすべてのトークンのK・Vを作成し、キャッシュに書き込みます。最初のトークンはここから出力されます。最初のトークンまでの時間(TTFT)を決定します。
  2. デコード(decode): その後、1トークンずつ生成します。トークン間の間隔(ITL・TPOT)を決定します。

両者は同じモデルを使用しますが、ハードウェア上での振る舞いは異なります。

おもちゃのモデルでもその違いが見られました(キャッシュあり、3回の中央値)。

プロンプト プリフィル トークンあたり デコード 1ステップ 倍率
64 0.018ms 0.445ms 24倍
256 0.034ms 0.506ms 15倍
1,024 0.144ms 0.852ms 6倍

プリフィル全体の時間は、64トークンで1.16ms、256トークンで8.76ms、1,024トークンで146.9msでした。

1つのトークンをデコードするのに、プリフィルのトークンあたりの時間の6〜24倍のコストがかかりました。CPUのnumpyを使用しているためPythonの呼び出しコストが混ざっており、1,024トークンのプリフィルではアテンションの2乗項が大きくなってトークンあたりのコストが上昇します。そのため、倍率そのものというよりは「一度にまとめて処理する方がトークンあたり圧倒的にコストが低い」という傾向に着目してください。

GPU環境ではこの差がさらに大きくなります。別シリーズの実測値(8GBクラスのGPU、Qwen3-1.7B、vLLM)では、入力処理速度は約8,200トークン/秒であり、1つのリクエストをデコードする際のトークン間隔は15ms(約66トークン/秒)でした。 トークンあたり120倍以上の差が生じます。

デコードがメモリに制約されている場合、スループットを向上させる方法は1つしかありません。 重みを1回読み込む間に複数のリクエストを同時にデコードすること、すなわちバッチサイズを大きくすることです。同じ実測値において、同時リクエストを1から8に増やすとスループットが66から383トークン/秒(5.8倍)に、32から831トークン/秒に増加しました。しかし、バッチに含まれるリクエストごとに自身のKVがGPUメモリ上に存在していなければなりません。入力512 + 出力128トークンであれば1つのリクエストが640トークンとなり、そのGPUのKV領域(22,048トークン)には約34個が入ります。同時に64個を送信したところ、スループットは向上せず、最初のトークンの時間が5.7秒に跳ね上がりました。 スループットの上限を決定づけているのはKVメモリなのです。

7. まとめ:このシリーズで追っていく疑問

事実 そこから生じる疑問 取り上げる内容
KVは変化しないため、保存して再利用する — 第1部
デコードはバッチが大きいほど効率的である KVメモリを無駄なく使用してバッチサイズを大きくするには? 第2部(PagedAttention、連続バッチ処理)
前半部分が同じであればKVも同じである 他のリクエストのKVを再利用するには? 第2部(プレフィックスキャッシュ)、第3部
GPUメモリは小さい KVをCPU・ディスク・他のサーバーに置いて読み込むには? 第3部(LMCache)
プリフィルとデコードは性格が異なる 両者を異なるGPUに振り分けるには? すでにKVが存在する場所に送信するには? 第4部(Dynamo)

T.T.の一言。 卓球の試合で、得点を毎回最初のサーブから数え直す審判はいません。スコアボードに記録しておき、1点ずつ足していきますよね。KVキャッシュがまさにそのスコアボードです。ただし、試合会場が数百個もあると、スコアボードを置く場所から足りなくなってくるのです。

参考

ログインするといいねできます

コメント

まだコメントはありません。

ログインするとコメントできます