結論

LLM の推論が速くなるのは、KV キャッシュで「過去のトークンの計算のやり直し」を省き、投機的デコーディングで「本体を呼ぶ回数」を減らすからです。numpy で自作した小さな Transformer では、KV キャッシュで256文字の生成が6.50秒から0.18秒になり、投機的デコーディングは出力の分布を変えずに本体の呼び出しを約3分の1にしました。

この記事でわかること

  • KV キャッシュ有り・無しのトークンあたりの時間と計算量の実測(系列長16〜511)
  • KV キャッシュのメモリの式と、公式の設定値で試算した4モデルの容量
  • 投機的デコーディングの受理・棄却の式(原論文で確認)と、受理率・速度向上の実測

2026年11月4日更新|Python 3.13.18・numpy 2.5.3(OpenBLAS 0.3.34)・CPU 2コアで実行確認

この記事で実際に動かしたもの:numpy だけで書いたデコーダのみの Transformer(文字単位、本体 51万パラメータ・ドラフト 6万パラメータ)を手書きの逆伝播で学習させ、KV キャッシュ・プリフィル・接頭辞の再利用・KV の量子化・GQA・投機的デコーディングを実装して時間と一致を測りました。

LLM は1トークンずつ文を作ります。何も工夫しないと、トークンを1つ足すたびに、それまでの文を最初から全部計算し直すことになります。KV キャッシュは「前に計算した途中結果を取っておく」工夫、投機的デコーディングは「小さなモデルに先読みさせ、大きなモデルはまとめて答え合わせだけする」工夫です。

flowchart TD
    P[プロンプト全体] --> F[プリフィル<br>全トークンを1回で計算]
    F --> KV[(KV キャッシュ<br>層ごとの K と V)]
    F --> T1[最初のトークン]
    T1 --> D[デコード<br>新しい1トークンだけ計算]
    KV --> D
    D -->|K と V を追加| KV
    D --> T2[次のトークン]
    T2 -->|終わるまで繰り返す| D

よく調べられることの答えを先にまとめます。

知りたいこと 答え 詳しい節
KV キャッシュ(LLM)とは 各層の Key と Value を保存し、次のトークンでは新しい1個分だけ計算する仕組み KVキャッシュとは
KV キャッシュでどれだけ速くなるか 手元の実測で、系列長511のとき1トークンあたり92倍。生成全体で36倍 KVキャッシュ有り無しの実測
KV キャッシュのメモリの式 層数×2×KVヘッド数×ヘッド次元×系列長×バイト数(×バッチ) KVキャッシュのメモリ
8B 級のモデルの KV キャッシュ Qwen3-8B は bf16 で1トークン144KiB。32Kトークンで4.5GiB KVキャッシュのメモリ
投機的デコーディング(speculative decoding)とは 小さなドラフトが k 個提案し、本体が1回でまとめて検証する方式 投機的デコーディングとは
出力は変わらないのか 受理・棄却の規則で、本体だけで作った場合と同じ分布になる(原論文の定理) 受理・棄却の式
どれだけ速くなるか 受理率と、ドラフトの重さ・検証の重さで決まる。手元の CPU では最大1.29倍 投機的デコーディングの実測
LLM 推論の高速化の手段の選び方 長い文脈は GQA・KV の量子化、同時接続はページング、1本の速さは投機的デコーディング 高速化の判断表

確認環境:Ubuntu 24.04.5 LTS(Intel Xeon 2.10GHz・2コア、ほかの作業と同時に動いている共有のマシン)の上で、Python 3.13.18 と numpy 2.5.3(OpenBLAS 0.3.34、スレッド数1に固定)だけを使い、デコーダのみの Transformer(Pre-LN・学習する位置埋め込み・GELU)を自分で書きました。PyTorch などの深層学習ライブラリは使っていません。学習データは Python 3.13 標準ライブラリのソースコード71ファイル(2,054,666文字、96種類の文字)で、本体(4層・幅96・4ヘッド・513,600パラメータ)を5分25秒、ドラフト(1層・幅48・2ヘッド・61,968パラメータ)を33秒学習させました。学習済みの公開モデルの重みや LLM の API には接続していないため、出力の文章の質については主張しません。測ったのは時間・一致・受理率です。時間は7〜51回の中央値で、共有のマシンのため数%〜十数%ぶれます。主要な公開モデルの設定値は Hugging Face 上の各社公式リポジトリの config.json、投機的デコーディングの式は Leviathan et al.(ICML 2023)と Chen et al.(2023)の原論文、PagedAttention は Kwon et al.(SOSP 2023)、GQA は Ainslie et al.(2023)、量子化 KV は vLLM の公式ドキュメントで確かめました。図は mermaid 11.16.0 で描画を確かめています(確認日 2026年11月4日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「LLM推論の高速化|KVキャッシュと投機的デコーディングを自作で実測」

LLMの推論が遅い理由(プリフィルとデコード)

この節では、LLM の推論のどこに時間がかかるのか、プリフィルとデコードの2つの段階に分けて分かります。

LLM(デコーダのみの Transformer)は、次の1トークンの確率を出し、1つ選んで文に足し、また次を出す、を繰り返します(自己回帰生成)。このとき処理は2つの段階に分かれます。

プリフィル(prefill)

プロンプトの全トークンを1回でまとめて計算する

行列と行列の積になり、計算機の演算器をよく使える

最初の1トークンが出るまでの時間(TTFT)を決める

デコード(decode)

新しいトークンを1個ずつ計算する

1行の行ベクトルと重み行列の積になり、重みを読む時間が主になる

2トークン目以降の1トークンあたりの時間(TPOT)を決める

デコードでは、1トークンを作るたびにモデルの全部の重みをメモリから読み出しますが、その重みで行う計算は1行分しかありません。そのため GPU では、計算の速さよりメモリから読み出す速さ(帯域)で時間が決まりがちです。この「1回の呼び出しで重みを読むなら、1トークン分でも数トークン分でもあまり時間が変わらない」という性質が、後で説明する投機的デコーディングの前提になります。

もう1つの重さは注意(Attention)です。位置 T のトークンは、それより前の T 個のトークン全部の Key と Value を参照します。キャッシュが無ければ、その Key と Value を毎回作り直すことになります。

KVキャッシュとは(何を保存して何を省くか)

この節では、KV キャッシュが保存しているものと、それで省ける計算が分かります。

KV キャッシュは、各層の注意で使う Key(K)と Value(V)のベクトルを、トークンごと・層ごとに保存しておく仕組みです。デコーダのみの Transformer は因果マスク(前のトークンしか見ない)を使うため、過去のトークンの K と V は、後ろにトークンが足されても変わりません。変わらないものは1回作って取っておけばよい、というのが KV キャッシュの考え方です。

1保存するのは K と V

Query(Q)は新しいトークンの分だけあれば足りるので保存しない。保存するのは過去の全トークンの K と V で、層の数だけある。

2省けるのは過去の再計算

キャッシュが無いと、位置 T のトークンを作るたびに T 個分の埋め込み・線形変換・MLP をやり直す。有れば新しい1個分だけになる。

3代わりにメモリを使う

系列長とバッチ数に比例してメモリが増える。長い文脈・多数の同時接続では、重みより KV キャッシュの方が大きくなることもある。

注意の計算そのもの(新しい Q と、過去の全 K の内積)は、キャッシュが有っても系列長に比例して増えます。キャッシュで消えるのは「過去のトークンの K・V と MLP を作り直す分」で、注意で過去を参照する分は残ります。下の実測でも、キャッシュ有りの計算量は系列長とともに少しずつ増えています。

numpyで小さなTransformerを自作する(実験の作り方)

この節では、実験に使ったモデルの作りと、KV キャッシュ付きの推論のコードが分かります。

学習済みの重みを取得できない環境のため、モデルは自分で作って学習させました。構成は GPT-2 と同じ型(Pre-LN・学習する位置埋め込み・GELU の MLP)を小さくしたものです。逆伝播も numpy で手書きし、数値微分との相対誤差が 6.8×10-6 以下であることを確かめてから学習しました。

本体(target) ドラフト(draft)
層数・幅・ヘッド数 4層・96・4ヘッド(ヘッド次元24) 1層・48・2ヘッド(ヘッド次元24)
パラメータ数 513,600 61,968
学習 400ステップ(16本×128文字)・5分25秒 同じ条件・33秒
検証データの損失 2.254(1文字あたり、自然対数) 2.341
1トークンのデコード時間 0.720ms 0.158ms(本体の0.22倍)

学習は数分で止めたので、文章(コード)の質は高くありません。貪欲法では同じ文字の繰り返しに陥ることもあります。この記事の測定は、モデルの賢さではなく推論の仕組みが時間と結果にどう効くかを見るためのものです。

KV キャッシュ付きの推論の中心は次の関数です。新しいトークンを1個でも複数でも受け取り、その分の K と V をキャッシュの続きに書き込み、注意はキャッシュの先頭から現在までを参照します。複数渡せばプリフィルになり、投機的デコーディングの一括検証にもそのまま使えます。

class KVCache:
    """層ごとに K と V を (H, maxT, D) で確保し、長さ n までを使う"""
    def __init__(s, cfg, maxT, dtype=np.float32):
        C, L, H = cfg["C"], cfg["L"], cfg["H"]; D = C // H
        s.K = [np.zeros((H, maxT, D), dtype) for _ in range(L)]
        s.V = [np.zeros((H, maxT, D), dtype) for _ in range(L)]
        s.n = 0
    def truncate(s, n):
        s.n = n  # 棄却したトークンの K/V は、長さを戻すだけで捨てられる

def forward_cached(P, cfg, new_toks, cache):
    n = len(new_toks); p0 = cache.n; Tt = p0 + n
    C, L, H = cfg["C"], cfg["L"], cfg["H"]; D = C // H
    # 新しい n 個のうち i 番目は、位置 p0+i までしか見ない(因果マスク)
    mask = np.triu(np.full((n, Tt), -1e9, np.float32), p0 + 1)
    x = P["wte"][new_toks] + P["wpe"][p0:Tt]
    for l in range(L):
        h, _ = ln_fwd(x, P[f"{l}.ln1_g"], P[f"{l}.ln1_b"])
        q, k, v = np.split(h @ P[f"{l}.wqkv"], 3, -1)
        cache.K[l][:, p0:Tt] = k.reshape(n, H, D).transpose(1, 0, 2)  # 足すだけ
        cache.V[l][:, p0:Tt] = v.reshape(n, H, D).transpose(1, 0, 2)
        q = q.reshape(n, H, D).transpose(1, 0, 2)
        att = softmax(q @ cache.K[l][:, :Tt].transpose(0, 2, 1) / np.sqrt(D) + mask)
        y = (att @ cache.V[l][:, :Tt]).transpose(1, 0, 2).reshape(n, C)
        x = x + y @ P[f"{l}.wo"]
        h2, _ = ln_fwd(x, P[f"{l}.ln2_g"], P[f"{l}.ln2_b"])
        x = x + gelu(h2 @ P[f"{l}.w1"] + P[f"{l}.b1"]) @ P[f"{l}.w2"] + P[f"{l}.b2"]
    cache.n = Tt
    hf, _ = ln_fwd(x, P["lnf_g"], P["lnf_b"])
    return hf @ P["wout"]          # 新しい n 個の位置それぞれの logits

キャッシュ無しの版は、同じ計算を毎回「系列の全トークン」に対して行い、最後の位置の logits だけを使います。まず、両者が同じ結果になるかを確かめました。プロンプト def __init__(self, から貪欲法で96文字を作った結果です。

numpy 2.5.3 python 3.13.18 params 513600 {'V': 96, 'C': 96, 'L': 4, 'H': 4, 'T': 512}
same tokens: True  max |logit diff|: 2.86e-06
generated: '    def __init__(self, telf, self, te):\n                    (以下、空白が続く)'

96文字すべてが一致し、logits の差は最大 2.86×10-6(float32 の丸め誤差の大きさ)でした。KV キャッシュは近似ではなく、同じ計算の順番を変えて重複を省いているだけです。

KVキャッシュ有り無しの実測(トークンあたりの時間と計算量)

この節では、系列長が伸びたときに KV キャッシュの有無で1トークンの時間と計算量がどう変わるかが、実測で分かります。

位置 T の1トークンを作る時間を、キャッシュ無し(T 個分を計算し直す)と有り(1個分)で測りました。計算量(MFLOP、100万回の浮動小数点演算)は、行列の積の大きさから数えた値です。

T, nocache_ms, kv_ms, ratio, nocache_MFLOP, kv_MFLOP
  16     4.02   0.950    4.2x     15.1   0.95
  32     5.94   0.648    9.2x     30.5   0.98
  64    11.09   0.611   18.1x     62.6   1.03
 128    22.36   1.072   20.9x    131.6   1.13
 256    50.61   0.692   73.1x    288.3   1.32
 511   106.57   1.157   92.1x    675.6   1.71
generate 256: nocache 6.50s  kv 0.18s
系列長 T キャッシュ無し キャッシュ有り 差 計算量(無し→有り)
164.02ms0.950ms4.2倍15.1→0.95 MFLOP
6411.09ms0.611ms18.1倍62.6→1.03 MFLOP
12822.36ms1.072ms20.9倍131.6→1.13 MFLOP
25650.61ms0.692ms73.1倍288.3→1.32 MFLOP
511106.57ms1.157ms92.1倍675.6→1.71 MFLOP
6.50秒256文字の生成(キャッシュ無し)プロンプト16文字・3回の中央値
0.18秒256文字の生成(キャッシュ有り)約36倍速い
92倍系列長511での1トークンの差106.57ms と 1.157ms

読み取れることは3つです。

  • キャッシュ無しの時間は系列長にほぼ比例して増えます(T=128 で22ms、T=511 で107ms)。生成全体では各トークンの時間の合計なので、系列長の2乗に近い形で増えます。
  • キャッシュ有りの時間はほぼ一定です(0.6〜1.2ms)。計算量は T=16 の 0.95 MFLOP から T=511 の 1.71 MFLOP へ増えますが、これは注意で過去の K を参照する分で、この小さなモデルでは Python と numpy の呼び出しの手間に隠れています。
  • キャッシュ有りの時間のばらつき(0.61〜1.16ms)は、共有のマシンでほかの処理と CPU を分け合っているための揺れです。傾向を見るときは中央値の並びで判断しています。

大きなモデルでは、キャッシュ有りでも注意の参照分(KV キャッシュの読み出し)が長い文脈で重くなります。次の節以降で扱う GQA や KV の量子化は、この読み出しの量を減らす工夫です。

プリフィルとデコードの速さの違い(接頭辞の再利用も実測)

この節では、同じトークン数でも「まとめて入れる」か「1個ずつ入れる」かで速さが大きく違うことと、共通の接頭辞の KV を使い回す効果が分かります。

同じ64個・256個のトークンを、プリフィル(1回の呼び出しでまとめて)とデコード(1個ずつ呼ぶ)でキャッシュに入れたときの時間です。

feed 64 tokens: prefill 11.9ms (5386 tok/s)  one-by-one 51.1ms (1253 tok/s)
feed 256 tokens: prefill 46.6ms (5490 tok/s)  one-by-one 191.7ms (1335 tok/s)
prefix 384 + suffix 64: recompute 96.8ms  reuse 13.4ms  max|diff| 2.38e-06

結果の K・V は同じですが、まとめて入れると1秒あたりのトークン数が約4.1倍になりました。1個ずつだと、毎回の関数呼び出しと「重みを読んで1行だけ掛ける」処理が繰り返されるためです。GPU ではこの差がさらに大きく、プリフィルは演算器の速さ、デコードはメモリ帯域で決まる、という形で現れます。デコードの1回の呼び出しに、もう少しトークンを詰めても時間があまり増えないことが、投機的デコーディングで「k 個をまとめて検証する」意味につながります。

3行目は、共通の384トークン(システムプロンプトや長い資料を想定)の KV を1回だけ作って写し、続きの64トークンだけを計算した場合です。最初から448トークンを計算する 96.8ms に対し、使い回すと 13.4ms でした。結果の差は 2.38×10-6 で同じ計算です。vLLM の prefix caching など、推論サーバーが共通の接頭辞のキャッシュを再利用する機能はこの考え方です。各社の API にある「プロンプトキャッシュ」の料金や有効期間はサービスごとの決まりなので、この記事では扱いません。

KVキャッシュのメモリの式と主要モデルの試算

この節では、KV キャッシュのメモリを見積もる式と、公開モデルの公式の設定値で試算した容量が分かります。

KV キャッシュのバイト数 = 層数 × 2(K と V)× KV ヘッド数 × ヘッド次元 × 系列長 × 1要素のバイト数 × バッチ数です。手元のモデル(4層・4ヘッド・ヘッド次元24)に512トークン入れたときの配列の大きさと、式の値を比べました。

float32 cache bytes 1572864 formula 1572864
float16 cache bytes 786432 formula 786432

4 × 2 × 4 × 24 × 512 × 4バイト = 1,572,864 バイトで、実際の配列と一致します。float16 にすればちょうど半分です。

同じ式で、公開モデルを試算しました。層数・KV ヘッド数・ヘッド次元は、各社が Hugging Face で公開している公式リポジトリの config.json で確かめた値です(head_dim の項目が無いモデルは hidden_size ÷ num_attention_heads)。要素は bf16(2バイト)、バッチ1です。

モデル 層数・KVヘッド×次元 1トークンあたり 32,768トークン 補足
Qwen3-8B 36層・8×128 144KiB 4.5GiB 注意ヘッド32に対し KV ヘッド8(GQA)
Mistral-7B-v0.1 32層・8×128 128KiB 4.0GiB sliding_window 4096。窓の外を捨てる実装なら512MiBで頭打ち
Qwen2.5-72B-Instruct 80層・8×128 320KiB 10GiB 仮に64ヘッドすべてが KV を持つ(MHA)と80GiB
gpt-oss-20b 24層・8×64 24KiB(全体注意の12層分) 0.75GiB+3MiB 半分の12層は窓128トークンの注意で、合計3MiBで頭打ち

gpt-oss-20b は layer_types で「窓128の注意」と「全体の注意」が交互に並ぶ設定になっており、最大の131,072トークンでも全体の注意の12層分で約3.0GiBです。一方 Qwen3-8B を32,768トークン×16本で同時に動かすと、KV キャッシュだけで72GiBになります。同時接続数と文脈長を掛けると、KV キャッシュが重みより大きくなることが、推論サーバーのメモリ設計の中心になります。

KV キャッシュの見積もり

やってはいけない注意ヘッド数(num_attention_heads)で計算する。GQA のモデルでは数倍大きく見積もってしまう
→
代わりにnum_key_value_heads と head_dim を config.json で確かめ、窓付きの層があれば layer_types も見る

Qwen2.5-72B では、取り違えると10GiBが80GiBになる

GQA・MQAでKVキャッシュを減らす(読み出し時間の実測)

この節では、GQA(Grouped-Query Attention)と MQA(Multi-Query Attention)が KV キャッシュの容量と読み出し時間をどれだけ減らすかが分かります。

GQA は、複数の Query ヘッドで1組の K・V を共有する方式です。Ainslie et al.(2023)は、K・V を1組だけにする MQA はデコードを大きく速くするが品質が下がることがあるとし、その中間(1より多く Query ヘッド数より少ない KV ヘッド)として GQA を示しました。既存の MHA のモデルから、元の事前学習の約5%の計算で GQA に変換(uptraining)できる、とも書いています。上の表のモデルはどれも KV ヘッドが8で、Query ヘッドより少なくなっています。

注意の部分だけを取り出し、Query ヘッド8・ヘッド次元128・系列長4,096の1層で、KV ヘッドを8(MHA)・2(GQA)・1(MQA)にしたときのデコード1ステップの時間を測りました(乱数の重み、51回の中央値)。

kv_heads, cache_MB(1 layer, fp32), attention_step_ms (median of 51)
8 32.0 5.947
2 8.0 2.057
1 4.0 1.182
方式 KV ヘッド数 1層のキャッシュ 注意1ステップ
MHA832MB5.95ms
GQA28MB2.06ms
MQA14MB1.18ms

容量は KV ヘッド数に比例して減り、時間も 5.95ms から 2.06ms、1.18ms へ減りました。デコードの注意は「キャッシュを読む」処理なので、読む量が減ればそのまま速くなります。品質への影響は学習の仕方によるため、この実験では測っていません(乱数の重みで時間だけを測りました)。

PagedAttentionとKVキャッシュの量子化

この節では、vLLM の PagedAttention が減らす「メモリの無駄」と、KV キャッシュを float16・int8 に落としたときの影響が分かります。

ページング(PagedAttention)

Kwon et al.(SOSP 2023)は、リクエストごとの KV キャッシュは大きく、長さが動的に増減するため、管理が悪いと断片化や重複で無駄が出てバッチ数が制限される、と指摘しました。PagedAttention は OS の仮想メモリとページングに着想を得た注意のアルゴリズムで、vLLM は KV キャッシュの無駄をほぼ0にし、同じ遅延で FasterTransformer や Orca に比べてスループットを2〜4倍にした、と報告しています。

無駄の大きさの感覚をつかむため、長さ50〜2,047トークンの1,000リクエストを想定して、最大長(2,048)ぶんを先に確保する方式と、16・32トークンのブロック単位で確保する方式の無駄を数えました。

block none(prealloc 2048) waste 47.2%
block 16 waste 0.7%
block 32 waste 1.4%

最大長で確保すると半分近くが使われないまま埋まり、ブロック単位なら無駄は最後のブロックの空きだけになります。これは長さの分布を一様乱数にした単純な数え上げで、vLLM の実装そのものを動かした結果ではありません。vLLM の使い方は関連記事「vLLMとは|使い方・インストール・OpenAI互換APIとOllamaとの違い」で扱っています。

KV キャッシュの量子化(float16・int8)

手元のモデルで、キャッシュを float16 で保存した場合と、ヘッド・位置ごとの最大絶対値で int8 に丸めた場合(丸めてから戻す形で誤差を再現し、容量は int8 本体と float16 のスケールで数えた)を比べました。8本のプロンプトで96文字ずつ貪欲法で生成し、float32 のときと同じ文字列になるかを見ています。

dtype, bytes_at_112, greedy_same_as_fp32(8 prompts x 96), mean_first_diverge, max|logit diff| until diverge
float32 344064 8/8 96.0 0
float16 172032 8/8 96.0 0.000739
int8 93184 8/8 96.0 0.00885

int8 で容量は 27%(344,064→93,184バイト)になり、8本とも同じ文字列でした。logits の差は最大 0.0089 です。ただし、これは小さなモデルの短い生成での結果で、大きなモデル・長い文脈で品質が保たれるかは別に確かめる必要があります。vLLM の公式ドキュメントでは、kv_cache_dtype に "fp8"("fp8_e4m3")・"fp8_e5m2" を指定でき、KV キャッシュに使える領域がおよそ2倍になる一方、量子化を戻す処理と注意の融合が未実装のため遅延は改善しない(効果はスループット側)、と説明しています。GGUF の量子化(重みの量子化)は関連記事「llama.cppとは|使い方・ビルド・GGUF量子化とllama-server」で扱っています。

投機的デコーディングとは(speculative decoding の仕組み)

この節では、投機的デコーディングがなぜ本体の呼び出し回数を減らせるのか、流れとして分かります。

投機的デコーディング(speculative decoding、Chen et al. は speculative sampling と呼ぶ)は、小さく速いドラフトモデルに k 個のトークンを先に提案させ、本体(ターゲット)はその k 個を1回の呼び出しでまとめて検証する方式です。検証で前から順に受理し、最初に棄却したところで止めて、そこだけ本体の分布から引き直します。

flowchart TD
    S[確定した文] --> Q[ドラフトが<br>k 個を提案]
    Q --> V[本体が k+1 位置を<br>1回で計算]
    V --> J{前から順に<br>受理・棄却}
    J -->|全部受理| A[k 個<br>+本体の1個]
    J -->|i 番目で棄却| R[i-1 個<br>+引き直し1個]
    A --> S
    R --> S
1

ドラフトが k 個を提案する

小さなモデルで、ふつうの自己回帰生成を k 回行う。各位置の確率分布 q も控えておく。

ドラフトを k 回呼ぶ

2

本体が k+1 位置を1回で計算する

確定済みの最後のトークンと提案 k 個を、KV キャッシュ付きでまとめて入れる。プリフィルと同じ形なので、1トークン分と比べて時間がそれほど増えない。各位置の分布 p が一度に出る。

本体を1回呼ぶ

3

前から順に受理・棄却する

乱数 r が min(1, p(x)/q(x)) より小さければ受理。棄却したら、その位置で norm(max(0, p−q)) から1個引き直して止める。

次の節の式

4

全部受理なら本体からもう1個

k 個すべて受理したら、本体がすでに計算した k+1 番目の分布からもう1個引く。1回の本体の呼び出しで、最少1個・最多 k+1 個が確定する。

最多 k+1 トークン

5

棄却した提案の KV を捨てる

本体のキャッシュには棄却した提案の K・V も入っているので、長さを受理した位置まで戻す。ドラフトのキャッシュも同じ長さにそろえる。

truncate

手元で書いた検証の部分は次のとおりです(温度0の貪欲法の分岐は省略)。

# ドラフトが kk 個を提案(xs: 提案したトークン、qs: そのときのドラフトの分布)
base = tc.n
lgs = forward_cached(PT, ct, [seq[-1]] + xs, tc)   # 本体を1回だけ呼ぶ
nacc = 0
for i in range(kk):
    p = probs(lgs[i], temp)
    if not rng.random() < min(1.0, p[xs[i]] / qs[i][xs[i]]):   # r < min(1, p(x)/q(x))
        break
    nacc += 1
p = probs(lgs[nacc], temp)
if nacc < kk:                          # 棄却:norm(max(0, p - q)) から引き直す
    r = np.maximum(p - qs[nacc], 0); r = r / r.sum()
else:                                  # 全部受理:本体の次の分布からもう1個
    r = p
t = int(rng.choice(len(r), p=r))
seq += xs[:nacc] + [t]
tc.truncate(base + 1 + nacc)           # 棄却した提案の K/V を捨てる
dc.truncate(min(dc.n, len(seq) - 1))

受理・棄却の式と出力の分布が変わらない理由

この節では、投機的デコーディングの受理・棄却の式を原論文の記号で確かめ、出力の分布が本体と同じになる理由と、20万回の標本での確認結果が分かります。

2本の原論文は、本体とドラフトの記号が逆になっています。読み比べるときに混乱しやすい点です。

Leviathan et al. 2023(ICML) Chen et al. 2023(DeepMind)
本体(ターゲット) Mp(分布 p) q
ドラフト Mq(近似モデル、分布 q) p
受理の条件 q(x) ≤ p(x) なら受理。q(x) > p(x) なら確率 1−p(x)/q(x) で棄却 r < min(1, q(x)/p(x)) なら受理
棄却したとき p'(x) = norm(max(0, p(x) − q(x))) から引く (q(x) − p(x))+ を正規化した分布から引く
全部受理したとき pγ+1 から1個引く 本体からもう1個引く

この記事では Leviathan et al. の記号(本体 p・ドラフト q・提案数 γ を k と書く)にそろえます。両論文の規則は同じで、「受理の確率は min(1, p(x)/q(x))、棄却したら max(0, p−q) を正規化した分布から引く」です。

分布が変わらない理由

ドラフトから x を引き、上の規則で受理・棄却したときに、最終的に x が出る確率を足し合わせます。

  • 受理の経路:q(x) × min(1, p(x)/q(x)) = min(p(x), q(x))
  • 棄却の経路:棄却が起きる確率は 1 − Σy min(p(y), q(y)) = Σy max(0, p(y) − q(y))。これに引き直しの分布 max(0, p(x) − q(x)) ÷ Σy max(0, p(y) − q(y)) を掛けると max(0, p(x) − q(x))
  • 合計:min(p(x), q(x)) + max(0, p(x) − q(x)) = p(x)

どの x についても本体の確率 p(x) に一致します。Chen et al. はこれを Theorem 1(Modified Rejection Sampling は目標の分布を復元する)として示しています。ドラフトの質は「速さ」にだけ効き、「出力の分布」には効きません。

20万回の標本で確かめる

3つの接頭辞について、本体の分布 p とドラフトの分布 q を出し、受理・棄却を20万回行った結果の分布と p との全変動距離(TVD、0なら一致)を測りました。比較として、本体から直接20万回引いたときの TVD(標本の揺れの大きさ)と、ドラフトの提案をそのまま使った場合の TVD も並べます。

'import '            accept=0.8987 theory_sum_min(p,q)=0.8991 TVD(spec,p)=0.0067 TVD(direct,p)=0.0056 TVD(draft_only,p)=0.1000
'    return self.'   accept=0.8697 theory_sum_min(p,q)=0.8706 TVD(spec,p)=0.0051 TVD(direct,p)=0.0045 TVD(draft_only,p)=0.1304
'        if not '    accept=0.8610 theory_sum_min(p,q)=0.8609 TVD(spec,p)=0.0062 TVD(direct,p)=0.0058 TVD(draft_only,p)=0.1389
接頭辞 受理率(実測/理論 Σmin) 投機的の TVD 本体から直接 ドラフトのみ
import の後0.8987/0.89910.00670.00560.1000
return self. の後0.8697/0.87060.00510.00450.1304
if not の後0.8610/0.86090.00620.00580.1389

投機的デコーディングの TVD(0.005〜0.007)は、本体から直接引いたときの標本の揺れ(0.0045〜0.0058)と同じ大きさで、ドラフトだけを使った場合(0.10〜0.14)よりはるかに小さくなりました。受理率は Leviathan et al. の α = Σ min(p, q)(= 1 − DLK(p, q))と小数第3位まで一致しています。たとえば「return self. の後」で本体が「_」に 0.2406 を割り当てているのに対し、ドラフトは 0.2119 ですが、受理・棄却を通した結果は 0.2398 でした。

投機的デコーディングを実装して受理率と速度を実測

この節では、提案数 k を変えたときの受理率・本体1回あたりのトークン数・速度向上の実測と、論文の式との比較が分かります。

検証用テキストの行頭16文字を8本選び、それぞれ96文字(合計768トークン)を生成しました。基準は「本体だけで KV キャッシュ付きのデコード」です。まず、本体と ドラフトの1回の呼び出しの時間を測りました。

target 1tok 0.720ms | 2tok 0.936ms 5tok 1.687ms 9tok 2.193ms | draft 1tok 0.158ms  c=0.22

ドラフトの1回は本体の0.22倍(Leviathan et al. の c にあたる値)です。本体に9トークンを一度に入れると 2.19ms で、1トークンの3.0倍かかりました。この CPU と小さなモデルでは、検証のトークン数を増やすと時間がそれなりに増えることが、速度向上の上限を決めます。

== temperature 0.7  baseline 0.62s for 768 tokens (1241 tok/s)
k, alpha(per tested), accepted/proposed, tokens/target_call, formula E, ideal_factor(flat verify), speedup, identical_to_greedy
1 0.911 0.911 1.91 1.91 1.57x 1.19x
2 0.859 0.798 2.58 2.60 1.81x 1.29x
3 0.878 0.774 3.25 3.33 2.01x 1.28x
4 0.901 0.765 4.00 4.10 2.19x 1.24x
6 0.879 0.651 4.80 4.92 2.13x 0.92x
8 0.846 0.471 4.63 5.05 1.84x 0.77x
k 受理率 α 本体1回あたり(実測/式) 検証が一定なら 実測の速度向上
10.9111.91/1.911.57倍1.19倍
20.8592.58/2.601.81倍1.29倍
30.8783.25/3.332.01倍1.28倍
40.9014.00/4.102.19倍1.24倍
60.8794.80/4.922.13倍0.92倍
80.8464.63/5.051.84倍0.77倍

表の列の意味は次のとおりです。

  • 受理率 α:受理・棄却の判定をしたうち受理した割合。棄却の後ろの提案は判定しないので、「受理数 ÷ 提案数」(2列目の accepted/proposed)とは別に数えています。
  • 本体1回あたり:768トークン ÷ 本体の検証の呼び出し回数。式は Leviathan et al. の式(1)、期待トークン数 = (1 − αk+1) ÷ (1 − α) に実測の α を入れた値です。実測と式は 0.0〜0.4 の差で合いました(k=8 のずれは、最後の回で残りの文字数が k より少なくなる分などです)。
  • 検証が一定なら:Leviathan et al. の Theorem 3.8 の改善率 (1 − αk+1) ÷ ((1 − α)(kc + 1)) に、実測の α と c=0.22 を入れた値。本体の1回の時間が、検証するトークン数に関係なく一定だと仮定した上限です。
  • 実測の速度向上:基準の時間 ÷ 投機的デコーディングの時間(3回の中央値)。
1.29倍いちばん速かった k=2(温度0.7)k=3 でも1.28倍
3.25個本体1回で確定したトークン数(k=3)本体の呼び出しは約3分の1
8/8本温度0で本体だけの貪欲法と同じ文字列k=1〜8 のすべて

温度0(貪欲法)と温度1.0の結果も並べます。温度0では、受理の判定は「本体の argmax が提案と同じか」になり、8本×96文字が本体だけの貪欲法と完全に一致しました(identical_to_greedy が True)。

== temperature 0.0  baseline 0.54s for 768 tokens (1417 tok/s)
1 0.842 0.842 1.84 1.84 1.51x 0.83x True
2 0.841 0.759 2.50 2.55 1.77x 1.05x True
3 0.857 0.738 3.17 3.22 1.95x 1.24x True
4 0.827 0.654 3.52 3.55 1.89x 1.10x True
6 0.836 0.534 4.06 4.35 1.88x 0.96x True
8 0.841 0.467 4.52 4.97 1.81x 0.93x True

== temperature 1.0  baseline 0.63s for 768 tokens (1223 tok/s)
1 0.831 0.831 1.82 1.83 1.50x 1.14x
2 0.851 0.791 2.55 2.57 1.79x 1.15x
3 0.856 0.746 3.20 3.22 1.94x 1.26x
4 0.851 0.670 3.61 3.72 1.98x 1.15x
6 0.847 0.582 4.34 4.49 1.94x 0.99x
8 0.858 0.518 4.99 5.27 1.91x 0.88x

3つの温度とも、k=2〜3 で1.2〜1.3倍が上限で、k を6以上にすると基準より遅くなりました。本体1回あたりのトークン数は k とともに増えますが、ドラフトを k 回呼ぶ時間と、本体で k+1 トークンを検証する時間がそれ以上に増えるためです。「検証が一定なら」の列(1.5〜2.2倍)との差が、この CPU で検証の時間が一定にならない分です。GPU で大きなモデルを動かすと、デコードはメモリ帯域で決まるため検証の時間が一定に近づき、実測は上限に近づきます。Leviathan et al. は T5-XXL で、標準の T5X の実装に比べて2〜3倍の高速化を、同じ出力のまま得たと報告しています。

なお、受理率0.85〜0.91は高めの値です。学習データが Python のソースコードで、インデントの空白や「self.」のように予測しやすい文字が多く、本体とドラフトの差も小さい(検証の損失 2.254 と 2.341)ためです。本体とドラフトの能力差が大きい組み合わせや、自由な文章では受理率は下がります。

投機的デコーディングが効く場面・効かない場面

この節では、速度向上を決める3つの要素と、投機的デコーディングを使うかどうかの判断材料が分かります。

Leviathan et al. の式から、速度向上を決めるのは次の3つです。

1受理率 α

ドラフトと本体の分布の近さ(Σmin(p,q))。同じ系列のモデルの小さい版、同じ学習データのモデルほど高い。温度や文章の種類でも変わる。

2ドラフトの重さ c

ドラフト1回 ÷ 本体1回の時間。小さいほどよいが、小さすぎると α が下がる。手元では0.22。

3検証の重さ

本体に k+1 トークンを入れたときの時間が1トークンとどれだけ違うか。メモリ帯域で決まる環境ほど一定に近く、効果が大きい。

効きにくいのは、次のような場面です。

  • 大きなバッチで動かしている推論サーバー:多数のリクエストをまとめるとデコードも演算器の速さで決まるようになり、検証の時間が一定でなくなる。手元の CPU の実測と同じ状況になる。
  • 受理率が低い組み合わせ:α が0.5なら k=4 でも本体1回あたり約1.94トークンで、ドラフトの時間を足すと得が小さい。
  • メモリに余裕がない:ドラフトの重みと KV キャッシュの分のメモリが別に要る。

ドラフトを別のモデルにしない方法として、プロンプト中の文字列から次を探す n-gram(prompt lookup)、本体に予測用の頭を足す Medusa・EAGLE、複数トークンを予測する MTP などがあります。どれも「提案→本体でまとめて検証→受理・棄却」の骨組みは同じです。これらの手法の詳細は各論文と各推論エンジンの公式ドキュメントの範囲になるため、この記事では名前の紹介にとどめます。

LLM推論の高速化の判断表

困っている症状から、どの高速化の手段を検討するかを表にまとめます。

症状・目的 まず検討する手段 効く理由 この記事の実測
生成が長くなるほど1トークンが遅くなる KV キャッシュ(推論エンジンでは標準) 過去のトークンの再計算を省く 系列長511で92倍
最初の1トークンが遅い(TTFT) 共通の接頭辞の再利用(prefix caching) 同じプロンプト部分のプリフィルを省く 96.8ms→13.4ms
長い文脈でメモリが足りない GQA のモデルを選ぶ・KV の量子化 KV のヘッド数・バイト数が減る KV ヘッド8→2で容量1/4、int8で27%
同時接続を増やすとメモリが足りない ページング(PagedAttention) 最大長の先取りによる無駄が消える 無駄47.2%→0.7%(数え上げ)
1本の応答の速さを上げたい 投機的デコーディング 本体の呼び出し回数が減る 本体1回で約3.25トークン、1.28倍
デコードの注意が長い文脈で重い GQA・MQA・窓付きの注意の層 読むキャッシュが減る 5.95ms→1.18ms(MQA)

なお、これらは組み合わせて使われます。たとえば推論サーバーでは、KV キャッシュとページングは前提として入っており、その上で KV の量子化や投機的デコーディングを設定で選ぶ形になります。

よくあるエラー・つまずき(自作するときの原文つき)

この節では、KV キャッシュや投機的デコーディングを自分で書いたときに出たエラーと、結果が合わなくなる原因が分かります。

cache overflow -> ValueError: could not broadcast input array from shape (4,23,24) into shape (4,9,24)
position overflow -> ValueError: operands could not be broadcast together with shapes (690,96) (512,96)
unnormalized p -> ValueError: Probabilities do not sum to 1. See Notes section of docstring for more information.
forgot truncate -> same as reference: False, first diff at 4
no causal mask in verify -> max|logit diff| 0.982, argmax same False
症状・エラー原文 原因 直し方
ValueError: could not broadcast input array from shape (4,23,24) into shape (4,9,24) 確保したキャッシュの長さ(32)を超えて書き込んだ 最大長を先に決めて確保するか、ブロック単位で足す
ValueError: operands could not be broadcast together with shapes (690,96) (512,96) 位置埋め込みの長さ(512)を超える系列を入れた 入力を最大長で切る。モデルの最大長は config.json の max_position_embeddings
ValueError: Probabilities do not sum to 1. float32 の softmax を float64 にしただけで正規化し直さず、rng.choice に渡した float64 で softmax を取り、p / p.sum() で正規化してから渡す
温度0なのに本体だけの貪欲法と結果が違う(4文字目からずれた) 棄却した提案の K・V をキャッシュに残したまま次へ進んだ 検証の後、本体のキャッシュの長さを受理した位置まで戻す(truncate)
一括検証の logits が1トークンずつの結果と違う(差 0.982) k+1 個をまとめて入れるときに因果マスクを付けなかった 新しい i 番目は位置 p0+i までしか見ないマスクを付ける
受理率が式と合わない 受理数 ÷ 提案数で数えた(棄却の後ろの提案も分母に入る) 受理数 ÷ 判定した数で α を数える
キャッシュ無しと有りで logits が 10-6 ほど違う 計算の順番が違うことによる float32 の丸め誤差 異常ではない。生成したトークンが同じかで確かめる

3つ目のエラーは、この記事の実験を書いている途中で実際に出たものです。温度つきのサンプリングで、全部受理した後に本体の分布をそのまま rng.choice に渡したところで止まりました。

よくある質問

この節では、KV キャッシュと投機的デコーディングについて、よく聞かれる疑問への答えが分かります。

Q. KV キャッシュを使うと出力は変わりますか。

変わりません。同じ計算の重複を省くだけです。手元では96文字が完全に一致し、logits の差は最大 2.86×10-6(丸め誤差)でした。KV キャッシュを量子化した場合は、値が丸められるので近似になります。

Q. なぜ Q(Query)はキャッシュしないのですか。

過去のトークンの Query は、次のトークンの計算で使わないからです。新しいトークンの注意に要るのは、新しいトークンの Q と、過去の全トークンの K・V です。

Q. KV キャッシュの容量はどう見積もればよいですか。

層数×2×KV ヘッド数×ヘッド次元×系列長×バイト数×バッチ数です。KV ヘッド数は num_attention_heads ではなく num_key_value_heads を使います。Qwen3-8B(36層・8×128)なら bf16 で1トークン144KiBです。

Q. 投機的デコーディングで出力の質は下がりませんか。

受理・棄却の規則を正しく実装すれば、出力の分布は本体だけの場合と同じです(Chen et al. の Theorem 1)。手元の20万回の標本でも、本体との差は標本の揺れと同じ大きさでした。温度0なら文字列が完全に一致します。

Q. ドラフトは何でもよいのですか。

出力の分布はドラフトに左右されませんが、速さは受理率に左右されます。語彙(トークナイザー)が本体と同じで、本体と分布が近く、十分に軽いことが条件です。

Q. 提案数 k はいくつがよいですか。

受理率と、ドラフト・検証の重さで決まります。手元の CPU では k=2〜3 が速く、6以上は基準より遅くなりました。実際の入力で受理率を測り、k を変えて時間で比べるのが確実です。

Q. プリフィルとデコードで速さが違うのはなぜですか。

プリフィルは多数のトークンをまとめて行列の積にできるのに対し、デコードは1トークンずつで、重みを読む割に計算が少ないからです。手元では同じ256トークンでも、まとめて入れると約4.1倍速くなりました。

Q. CPU でも投機的デコーディングは効きますか。

手元の小さなモデルでは1.2〜1.3倍でした。本体に入れるトークン数を増やすと時間が増える環境では、効果が小さくなります。大きなモデルほどデコードがメモリの読み出しで決まり、効果が出やすくなります。

Q. GQA は品質に影響しますか。

Ainslie et al. は、MHA の品質に近いまま MQA に近い速さが得られたと報告しています。手元の実験は時間と容量だけを測ったもので、品質は測っていません。

Q. LLM そのものの仕組みから知りたいときは。

トークンや確率で次を選ぶ仕組みは関連記事「生成AI(LLM)の仕組み8選|ハルシネーション・トークン」で扱っています。トークンの数え方は関連記事「トークンとは|生成AIの数え方・日本語の文字数目安と節約方法」で、Attention の計算そのものは関連記事「ニューラルネットワークとは|仕組み・学習・AttentionをNumPyで実演」で扱っています。

まとめ

この節では、記事全体の要点が分かります。

  1. KV キャッシュは過去のトークンの K・V を保存し、再計算を省く。出力は変わらず、手元では系列長511で1トークン92倍、256文字の生成で36倍速くなった。
  2. 代わりにメモリを使う。層数×2×KV ヘッド数×ヘッド次元×系列長×バイト数で見積もり、KV ヘッド数は config.json の num_key_value_heads で確かめる。
  3. GQA・KV の量子化・ページングは KV キャッシュの容量と無駄を減らす。手元では KV ヘッド8→1で注意1ステップが5.95ms→1.18ms、int8 で容量27%、ブロック確保で無駄47.2%→0.7%。
  4. 投機的デコーディングは r < min(1, p(x)/q(x)) で受理し、棄却したら norm(max(0, p−q)) から引く。これで出力の分布は本体と同じになり、20万回の標本でも確かめられた。
  5. 速さは受理率・ドラフトの重さ・検証の重さで決まる。式(1)は実測の本体1回あたりのトークン数とよく合い、手元の CPU では k=2〜3 で1.2〜1.3倍、k を増やしすぎると遅くなった。

参考・出典

この節では、この記事で根拠にした論文・公式ドキュメント・公式の設定ファイルが分かります(確認日 2026年11月4日)。

  • Yaniv Leviathan, Matan Kalman, Yossi Matias「Fast Inference from Transformers via Speculative Decoding」ICML 2023:https://arxiv.org/abs/2211.17192
  • Charlie Chen ほか「Accelerating Large Language Model Decoding with Speculative Sampling」DeepMind, 2023:https://arxiv.org/abs/2302.01318
  • Woosuk Kwon ほか「Efficient Memory Management for Large Language Model Serving with PagedAttention」SOSP 2023:https://arxiv.org/abs/2309.06180
  • Joshua Ainslie ほか「GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints」2023:https://arxiv.org/abs/2305.13245
  • vLLM 公式ドキュメント「Quantized KV Cache」:https://docs.vllm.ai/en/latest/features/quantization/quantized_kvcache.html
  • Qwen3-8B config.json(Qwen 公式):https://huggingface.co/Qwen/Qwen3-8B
  • Qwen2.5-72B-Instruct config.json(Qwen 公式):https://huggingface.co/Qwen/Qwen2.5-72B-Instruct
  • Mistral-7B-v0.1 config.json(Mistral AI 公式):https://huggingface.co/mistralai/Mistral-7B-v0.1
  • gpt-oss-20b config.json(OpenAI 公式):https://huggingface.co/openai/gpt-oss-20b

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「LLM推論の高速化|KVキャッシュと投機的デコーディングを自作で実測」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

成長のヒミツ、 ぜんぶ丸わかり