LLM 推論のプリフィルとデコード
一般的な LLM の推論が持つプリフィルとデコードという 2 つのフェーズについて、なぜ片方は計算が、もう片方はメモリの読み出しがボトルネックになりやすいのかを、KubeCon + CloudNativeCon Japan 2026 のセッションをきっかけに整理しました。
- 公開日
はじめに
KubeCon + CloudNativeCon Japan 2026 では、LLM 推論のパフォーマンス改善を扱うセッションがいくつかありました。 そこで前提のように使われていたのが、推論にはプリフィル(prefill)とデコード(decode)という 2 つのフェーズがあるという区別です。 同じモデルを同じ GPU で動かしているのに、前半は計算の速さが上限になり、後半はメモリの読み出しが追いつかなくなるのだといいます。 処理の途中でボトルネックが入れ替わるというこの話が、聞いた直後はピンと来ていませんでした。
本稿は、これを自分なりに納得できるところまで調べて整理した記録です。
1 トークンずつ進む生成
LLM は文章をひとかたまりで出力しているわけではなく、通常はトークン(単語や文字の断片に相当する単位)を 1 個ずつ生成しています。 入力と、それまでに出力したトークンを見て次の 1 個を選び、それを末尾に足してまた次を選ぶ、という繰り返しです。 チャットの応答が先頭から少しずつ表示されるのは読み込みの演出ではなく、多くのシステムが、この順番で生成されたテキストを、そばから逐次配信しているためです。
この繰り返しを素朴にやると、過去のトークンに対する計算を毎回やり直すことになります。 そこで実際の推論では、各トークンについて計算した中間結果を GPU のメモリに貯めておき、次のトークンを選ぶときに再利用します。 この貯めた中間結果が KV キャッシュです1。 プリフィルとデコードは、この KV キャッシュを「作る」フェーズと「使いながら 1 個ずつ伸ばす」フェーズ、と言い換えることもできます。 なお、ここから先の説明は、現在よく使われている構成を前提にします。 過去の全トークンを参照する密な decoder-only Transformer を、この KV キャッシュ付きの通常のデコードで動かす場合です2。
入力を並列に読むプリフィル
プリフィルは、利用者が送った入力(プロンプト)全体を処理して、KV キャッシュと最初の 1 トークンを作るフェーズです。 入力のトークンは最初からすべて手元にそろっているので、1 個ずつ順番に処理する必要がなく、全トークン分の計算を並列に流せます。
では、入力を読むだけの処理が、なぜ計算の速さで頭打ちになるのでしょうか。 GPU は、大きな行列計算をまとめて流すほど性能を発揮できるハードウェアです。 プリフィルはこの得意な形に合っているうえ、全トークンに同じ重みを適用するため、一度読み出した重みを数百から数千のトークンの計算に使い回せます。 入力が長いほど、重みの読み出し量あたりの演算量は増えます。 この比率が十分大きくなると、上限はメモリの読み出しから計算の速さへ移ります。 このように計算の速さそのものが上限になる状態を計算律速(compute-bound)と呼びます。 入力がある程度の長さを持つ典型的なプリフィルでは、GPU の計算器はほぼ休みなく働き続けます。 利用者の体感でいえば、プリフィルの長さは、送信してから最初の出力トークンが届くまでの待ち時間のうち、モデルの実行にあたる部分に現れます3。
出力を 1 トークンずつ書くデコード
デコードは、2 トークン目以降を 1 個ずつ生成していくフェーズです。 次のトークンは直前のトークンが決まらないと計算を始められないため、プリフィルのようにまとめて処理するわけにはいきません。
ここで効いてくるのが、1 トークン分の計算の少なさです。 1 トークンを生成するには、モデルの重み(パラメータ)全体と KV キャッシュを GPU のメモリから読み出す必要があります。 ところが、せっかく読み出した重みで行う計算は 1 トークン分しかありません。 たとえば密な 70 億パラメータのモデル(16 ビット精度で重みは約 14 GB)を、同時に処理するリクエストが 1 件だけの状態(バッチサイズ 1)で動かすとして、H100 SXM のカタログ値で見積もると次のようになります。
- 重みの読み出し:約 14 GB ÷ 毎秒約 3.35 TB で、約 4 ミリ秒
- 重みを使う計算:パラメータ数の約 2 倍のおよそ 140 億回の浮動小数点演算 ÷ FP16 の毎秒約 1,000 兆回(疎行列向けの倍増値ではない値)で、十数マイクロ秒
差はおよそ 300 倍。 どちらも理想条件の下限で、実測ではありません。 このほかに、読み出し時間には KV キャッシュの読み出しが、計算時間には attention の計算が、それぞれコンテキスト長に応じて加わります。 それでも、重みの読み出しが重みを使う計算より 2 桁以上重い、という見当を付けるには足ります。 同時に処理するリクエストが少ないうちは、計算器はほとんどの時間遊んでいて、メモリからの読み出しを待つことになります。 この状態がメモリ帯域律速(memory-bound)です。
「メモリがボトルネック」と聞くと容量不足を想像するかもしれませんが、ここで述べているのは別の制約です。 重みと処理中のリクエストの KV キャッシュがメモリに収まっている前提で、それでもなお、載っているものを 1 トークンごとに読み出す速さ(帯域)が上限になる、という話です。 容量も無関係ではなく、コンテキストが長くなったり同時のリクエストが増えたりすると KV キャッシュが膨らみ、同時に収容できるリクエスト数という別の制約として現れます。 デコードの速さは出力トークンが生成される速さを決め、利用者の体感では応答の文字が流れる速さとして現れます。
2 つのフェーズの対比
| プリフィル | デコード | |
|---|---|---|
| 役割 | 入力を読み、KV キャッシュを作る | 出力を 1 トークンずつ書く |
| 並列性 | 入力トークンをまとめて計算できる | 直前のトークン待ちで逐次 |
| 典型的な律速要因 | 計算の速さ(計算律速) | メモリの読み出し(メモリ帯域律速) |
| 体感に現れる場所 | 最初の出力トークンまでの待ち | 文字が流れる速さ |
もっとも、計算律速かメモリ帯域律速かは、フェーズの名前に貼り付いた性質ではありません。 読み出したデータ量に対してどれだけ計算するかという比率で決まるもので、入力の長さ、同時に処理するリクエストの数、モデルの形によって境界は動きます。 この表が示しているのは、その比率が極端に振れる典型的な場合です。
なぜこの区別が繰り返し語られるのか
性質がほぼ正反対だということは、片方に効く最適化がもう片方には同じようには効かないということでもあります。 たとえば複数の利用者のリクエストをまとめて処理する(バッチ処理)と、デコードでは一度読み出した重みを複数のリクエストで使い回せるようになり、読み出し待ちだった計算器に仕事を与えられます。 ただし、これで伸びるのはシステム全体が 1 秒に生成できるトークン数(スループット)で、利用者 1 人から見た文字の速さまで速くなるとは限りません。 一方のプリフィルは、入力が長ければ 1 件だけでも計算器を使い切れるため、まとめることによる伸びしろは相対的に小さくなります。
この違いを推し進めた構成として、プリフィルとデコードを別々の GPU 群に分けて処理する P/D 分離(disaggregated serving)があります。 性質の違う 2 つの処理を同じ GPU に同居させると、素朴なやり方では、長い入力のプリフィルが割り込むたびに他の利用者のデコードが待たされ、流れていた文字が止まります。 同居のまま抑える道もあります。 プリフィルを小さく刻んでデコードに混ぜる chunked prefill です。 一方、分けてしまえば干渉そのものがなくなり、フェーズごとに合った GPU の種類や台数を独立に選べます。 その代わり、プリフィル側で作った KV キャッシュをデコード側の GPU へ送るコストが新しく生まれます。 どちらが得かは、トラフィックやモデルに加えて、GPU 間の実効帯域や配置の条件で決まります。 Kubernetes の会議でこの区別が繰り返し語られるのは、どう分けてどうスケールさせるかを決めるのが、まさにインフラ側の仕事だからでしょう。
おわりに
「同じ推論なのに途中でボトルネックが入れ替わる」という冒頭の疑問には、こう答えられます。 典型的な条件では、読み出したデータ量あたりの計算量が、入力を並列に読むフェーズと出力を 1 トークンずつ書くフェーズとで極端に違うからです。 この区別を頭に置いてからセッションの資料を読み直すと、KV キャッシュの管理やバッチ処理、P/D 分離といった個々の手法が「どちらのフェーズの、どちらの資源を救う話か」として整理できるようになりました。
参考リンク
- Mastering LLM Techniques: Inference Optimization(NVIDIA Technical Blog)
- NVIDIA H100 Tensor Core GPU(仕様)
- Optimization and Tuning(vLLM ドキュメント)
- DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving(arXiv)
- llm-d(Kubernetes ネイティブの分散推論フレームワーク)
Footnotes
-
Transformer の attention という仕組みで使う Key と Value の 2 種類の値を貯めることから、この名前が付いています。詳細に立ち入らなくても、「過去分の計算結果の貯金」という理解でこの先の話は追えます。 ↩
-
「密」は、1 トークンの計算にパラメータ全体を使う構成のことです。一部しか使わない MoE(Mixture of Experts)では「重み全体を読む」が、小さなモデルが作った候補列を本体のモデルが並列に検証して一度に複数トークンを確定しうる投機的デコードでは「必ず 1 トークンずつ」が、それぞれそのままは成り立ちません。参照範囲を直近の窓に限る sliding-window attention のような構成も、「過去の全トークンを参照する」という前提の外にあります。 ↩
-
この「最初の出力トークンが届くまでの時間」を計測する指標が TTFT(Time to First Token)です。TTFT にはプリフィルのほかに順番待ちや通信の時間も含まれ、混雑時にはそちらが支配的になることもあります。画面に最初の文字が現れるまでには、さらにクライアント側の描画が挟まります。 ↩