Prefillとdecodeの違い、最初のtokenが遅い理由
Prefillは計算性能がTTFTを左右し、decodeはメモリ帯域幅がtokens per secondを左右します。両者を分けて測定する方法を解説します。
Prefill と decode の違いを 1 段落で説明
Prefill と decode の違いは、セルフホスト型 LLM(large language model)に関するレイテンシの疑問の大半を説明する重要な区別です。Prefill はプロンプト全体を 1 回の処理で読み込むため、制約要因は計算性能です。Decode は回答を 1 トークンずつ生成するため、制約要因はメモリ帯域幅です。Time to first token は prefill の指標です。Tokens per second は decode の指標です。
どちらのフェーズも、同じ GPU(graphics processing unit)上で、同じ重みを使い、同じプロセス内で実行されます。そのため、1 つのワークロードとして扱うのは自然です。ただし実際には、同じデバイスを共有する 2 つの異なるプログラムのように動作します。両者を分けて考えると、これまで分かりにくかった多くの結果を明確に説明できます。
Prefill が計算負荷律速になる理由
Prefill では、プロンプト全体をすべてのレイヤーに 1 回ずつ通します。2,000 token のプロンプトでは、各行列積に 2,000 行分の処理が発生します。そのため、GPU は読み込んだ重みの各バイトに対して大量の演算を実行します。移動した 1 バイトあたりの演算量を arithmetic intensity と呼びます。Prefill は arithmetic intensity が高い処理です。デバイスは計算性能の上限近くで動作し、メモリバスには余裕が残ります。
Prefill は 2 つのものを生成します。1 つはプロンプトの各 token に対応する KV cache(key と value のテンソル)です。もう 1 つは最初の出力 token です。この処理が完了するまで読み手には何も届きません。そのため、prefill time と time to first token (TTFT) はほぼ同じ値になります。
Prefill のコストはプロンプト長に応じて増加します。線形に増える部分は、各レイヤーで行う行列演算です。二次に増える部分は attention です。Attention では各 token が、それより前のすべての token を参照します。長いコンテキストでは、この部分の影響が大きくなります。そのため、プロンプトを 2 倍にすると、TTFT は少なくとも 2 倍になります。
この動作は 1 分で確認できます。サーバーに 200 token のプロンプトを送信し、次に 2,000 token のプロンプトを送信します。どちらも同じ数の出力 token を要求します。TTFT は大きく増加します。最初の token の後のストリーミング速度は、ほとんど変わりません。
デコードがメモリ帯域幅律速になる理由
デコードでは、1 ステップにつき 1 トークンを生成します。その 1 トークンを生成するには、GPU がモデル内のすべての重みをメモリから読み出し、各重みを数回の演算に使ってから破棄する必要があります。演算強度は 1 に近いため、演算ユニットは処理時間の大半を待機に費やします。
デコードが遅いのは、各トークンの生成時にモデル全体をメモリから読み出す必要があるためです。そのため、速度はメモリバスによって決まり、演算ユニットはアイドル状態になります。
このため、単一ストリームのデコード速度の上限は、紙上で計算できます。メモリ帯域幅を、重みが占めるバイト数で割ります。
The data behind this chart
[
{
"device": "CPU, dual channel DDR5-5600",
"mem_bandwidth_gb_s": 90,
"decode_ceiling_tok_s": 6
},
{
"device": "NVIDIA A10G",
"mem_bandwidth_gb_s": 600,
"decode_ceiling_tok_s": 38
},
{
"device": "NVIDIA L40S",
"mem_bandwidth_gb_s": 864,
"decode_ceiling_tok_s": 54
},
{
"device": "NVIDIA RTX 4090",
"mem_bandwidth_gb_s": 1008,
"decode_ceiling_tok_s": 63
},
{
"device": "NVIDIA A100 80GB SXM",
"mem_bandwidth_gb_s": 2039,
"decode_ceiling_tok_s": 127
},
{
"device": "NVIDIA H100 SXM",
"mem_bandwidth_gb_s": 3350,
"decode_ceiling_tok_s": 209
}
]帯域幅列には、各ベンダーが公開している仕様値を記載しています。上限列は、その値を 16 GB で割ったものです。16 GB は、16 ビット精度で保存した 8 billion パラメータモデルのサイズです。これは算術計算であり、ベンチマーク結果ではありません。実測値はこの上限を下回ります。どの程度下回るかを把握すると、サービングスタックを改善すべきか、ハードウェアを改善すべきかを判断できます。
6 行を順に読むと、傾向は明確です。デュアルチャネル DDR5 の CPU は約 90 GB/s でデータを転送するため、そのモデルのデコード速度は毎秒約 6 トークンが上限になります。L40S は約 54 です。公開帯域幅が 3350 GB/s の H100 SXM は、約 209 に達します。
量子化がデコード速度に対する単一の施策として最も効果的なのも、このためです。同じモデルを 16 ビットではなく 8 ビットで保存すると、トークンごとに読み出すバイト数が半分になるため、上限はおよそ 2 倍になります。演算量は増えません。読み出すメモリ量が減るだけです。
自分のサーバーで各フェーズを測定する方法
Ollama はレスポンスボディに分割結果を返します。非ストリーミングの completion を要求し、カウンターを読み取ります。
curl -s http://localhost:11434/api/generate -d '{
"model": "llama3.2",
"prompt": "Explain memory bandwidth in two sentences.",
"stream": false
}' | jq '{prompt_eval_count, prompt_eval_duration, eval_count, eval_duration}'実際に pull 済みのモデルタグを使用してください。利用可能なタグは ollama list で確認できます。prompt_eval_count と prompt_eval_duration は prefill です。前者はプロンプトのトークン数、後者はその処理にかかった時間です。eval_count と eval_duration は decode です。所要時間の単位はナノ秒なので、decode 速度は eval_count / eval_duration * 1e9、prefill 速度は prompt_eval_count / prompt_eval_duration * 1e9 です。同じリクエストでは、prefill 速度が decode 速度を大幅に上回るはずです。この差が、ここで説明する内容の背景です。
vLLM などの OpenAI 互換サーバーでは、curl で最初のバイトまでの時間を測定できます。
curl -N -s -o /dev/null \
-w 'pretransfer %{time_pretransfer}s first_byte %{time_starttransfer}s\n' \
http://localhost:8000/v1/completions \
-H 'Content-Type: application/json' \
-d '{"model": "meta-llama/Llama-3.1-8B-Instruct", "prompt": "Explain memory bandwidth.", "max_tokens": 128, "stream": true}'time_starttransfer は最初のレスポンスボディのバイトが到着した時点です。そのため "stream": true と合わせると、接続確立を含む TTFT になります。time_pretransfer を引くと、接続確立にかかった時間を除外できます。初回の呼び出しにはモデルのコールドロードが含まれる場合があるため、2 回実行して 2 回目の結果を採用してください。
vLLM は /metrics に Prometheus メトリクスも公開します。curl -s http://localhost:8000/metrics | grep -E 'time_to_first_token|inter_token_latency' を実行すると、vllm:time_to_first_token_seconds と vllm:inter_token_latency_seconds のヒストグラムを取得できます。キューの深さには vllm:num_requests_running と vllm:num_requests_waiting、キャッシュの逼迫度には vllm:kv_cache_usage_perc を追加します。この 5 つの名前でダッシュボード全体を構成できます。
負荷がかかっている状態では、vllm bench serve --model <name> --num-prompts 200 --request-rate 4 が稼働中のサーバーを駆動し、パーセンタイル付きで最初のトークンまでの時間と出力トークンごとのレイテンシを報告します。2 つのフェーズが互いに影響し合う状況を確認するには、これが唯一の方法です。調整を始める前に、基準値を正確に測定してください。ローカル LLM で 1 秒あたりのトークン数を測定する方法の手順なら、再起動後も比較に使える基準値を取得できます。
長い system prompt は、なぜ最初のトークンを遅らせるのにストリーミング速度は遅らせないのですか?
system prompt は prefill 処理の対象であり、それ以外の処理ではないためです。プロンプトの残りの部分と同じパスで 1 回だけ処理され、最初のトークンが出力される前に完了します。その後は KV cache のエントリとして保持され、decode 処理では他の情報と同じように読み取られます。そのため、3,000 token の system prompt はすべてのリクエストで TTFT を増加させますが、tokens per second はほぼ変わりません。
正確には、ほぼ変わらないだけです。追加された KV cache のエントリは decode の各ステップで再び読み取られるため、プロンプトが非常に長いと decode も少し遅くなります。次のセクションで説明します。
同じ prefix の再計算を止めることが対策です。prefix caching に対応したサーバーは、共有 prefix の KV cache を保持して再利用します。そのため、同じ system prompt を含む 2 回目のリクエストでは、その部分の prefill を完全に省略できます。vLLM ではこれを automatic prefix caching と呼びます。デフォルト設定はリリースによって変更されているため、使用中のバージョンで vllm serve --help を確認してください。この GPU 内の KV cache は、API プロバイダーが課金対象とする prompt cache とは別のものです。どちらかをチューニングする前に、KV cache と prompt cache の違いを確認してください。
コンテキストが埋まるにつれてデコードが遅くなるのはなぜですか?
理由は2つあり、どちらも KV キャッシュに関係します。
1つ目は帯域幅です。デコードの各ステップで、アテンションはそれ以前のすべてのトークンのキーとバリューを読み取ります。重みはトークンごとに一定のコストです。一方、KV キャッシュは増加します。サイズはモデルの config.json から計算できます。トークンあたりのバイト数は、2 に num_hidden_layers、num_key_value_heads、ヘッド次元(hidden_size を num_attention_heads で割った値)、要素あたりのバイト数を掛けた値です。先頭の 2 は、キー1つとバリュー1つを数えています。
一般的な 8 billion パラメータの構成で、32 層、GQA(grouped query attention)によるキー・バリューヘッド数8、ヘッド次元128、16 bit 精度の場合、2 x 32 x 8 x 128 x 2 = 131,072 bytes です。これはトークンあたり約 128 KiB です。したがって、8,000 トークンの会話では、リクエストごとにおよそ 1 GB の KV キャッシュを保持します。
2つ目は容量です。この 1 GB のメモリは、重みや別ユーザーのコンテキストには使用できません。サーバーは起動時に KV プールのサイズを決定します。vLLM では --gpu-memory-utilization で設定します。プールが満杯になると、新しいリクエストは待機します。vllm:num_requests_waiting が上昇し、vllm:kv_cache_usage_perc がほぼ 1 のままになる状態が、この状況を正確に示します。スタックによっては、キューに入れる代わりに実行中のリクエストをプリエンプトし、後でキャッシュを再計算します。この場合、ユーザーにはストリームの途中で停止したように見えます。
長いコンテキストには2重のコストがかかります。開始時の prefill 処理が増え、回答中もトークンごとに読み取るメモリ量が増加します。
バッチ処理でスループットが向上し、テールレイテンシが悪化するのはなぜですか?
デコードは帯域幅律速であるため、リクエストを追加しても計算側の負荷はほとんど増えません。重みを 1 回読み込むだけで、バッチ内の各シーケンスに対して 1 トークンを生成できます。そのため、KV プールが枯渇するか、バッチが大きくなって再び計算律速になるまでは、バッチサイズにほぼ比例して総スループットが向上します。Continuous batching ではステップごとにバッチを組み直します。そのため、完了したリクエストが離れ、キュー内のリクエストが、ほかのリクエストの完了を待たずに加わります。
その代償はパーセンタイルに現れます。各ユーザーの次のトークンは、共有ステップ内で最も遅い処理が終わるまで待つことになります。そのため、p50(中央値)は許容範囲に収まっていても、p99(100 リクエスト中で最も遅い 1 リクエスト)は大きく伸びます。ユーザーが体感するのは p99 です。文章の途中で発生する停止時間に相当するためです。
Prefill が加わると、この傾向はさらに強くなります。大きなプロンプトがストリーム処理の途中で到着すると、デバイスを 1 回の長いステップで占有します。その結果、現在ストリーミング中のすべてのリクエストで遅延が発生します。Chunked prefill では、長いプロンプトを分割し、各部分をデコードのバッチに混在させることで、この影響の大部分を解消します。2026 年 8 月時点では、vLLM V1 engine がこれをデフォルトで実行し、--max-num-batched-tokens によってバランスを調整できます。vLLM のチューニングドキュメントでは、このトレードオフを明確に説明しています。2048 前後の小さい値では、prefill がデコードを中断する回数が減るため、トークン間レイテンシ(ITL)が向上します。一方、大きい値では、1 回のバッチに収まる prefill トークン数が増えるため、TTFT が向上します。この 1 つのフラグで、prefill と decode の比率を数値として指定できます。p99 を許容できなくなる境界は、キャパシティの問題です。1 台の self-hosted LLM が同時に処理できるユーザー数では、同じメトリクスを使ってこの問題を検討します。
より大きな GPU に替えても、処理が速くならないことがあるのはなぜですか?
大きい GPU は通常、計算性能が高いことを意味します。しかし、decode で必要なのは計算性能ではありません。
上の表の2つの行を比較してください。A100 80GB の公称帯域幅は 2039 GB/s で、L40S は 864 GB/s です。decode の上限も正確に対応し、前者は毎秒 127 tokens、後者は 54 です。RTX 4090 は多くの指標で非常に高速なカードですが、1008 GB/s により、上限は 63 になります。2枚のカードで他の仕様が異なっていても、単一ストリームの decode 性能は仕様表のメモリ帯域幅に沿って変化します。
したがって、decode を高速化する方法は2つあります。token あたりに読み込むバイト数を減らす方法(重みを量子化する、または小さいモデルを使う)と、より帯域幅の大きい GPU を使う方法です。Prefill はこれと逆です。計算性能を必要とするため、高速な GPU であれば長いプロンプトの TTFT を実際に短縮できます。最初の token が出るまで4秒かかることが問題なら、より高性能なハードウェアで改善できる可能性があります。テキストの生成速度が遅いことが問題なら、改善できない可能性が高いです。
Prefill と decode を別々の worker で実行すべきですか?
大規模な serving stack では、まさにこの構成を採用しています。この手法は prefill and decode disaggregation と呼ばれます。1 つの worker pool は prefill のみを実行し、別の pool は decode のみを実行します。前者が構築した KV cache は、高速な interconnect を介して後者へ転送します。これは、各フェーズが異なるハードウェアとスケジューリングを必要とするため機能します。Prefill は計算性能と大きな token batch を必要とします。Decode は帯域幅と多数の同時実行 sequence を必要とします。分離すると、各 pool を個別にスケールできます。また、1 つの巨大な prompt によって、実行中のすべての stream が停止することも防げます。
1 台の GPU を搭載した単一の VPS (virtual private server) では、ほとんどの場合、実施する価値はありません。1 台のデバイスを自身に対して分割することになり、pointer の受け渡しを、数 GB の cache をネットワーク転送する処理に変えてしまいます。この手法の効果が出るのは、各フェーズ専用にマシン全体を割り当てられるだけの accelerator があり、両方の pool を継続的な traffic で稼働させられる場合です。それ未満の規模であれば、chunked prefill により、ほぼ同じ分離効果を 1 つの flag で得られます。
数値が悪い場合に変更する項目
TTFT が高すぎる場合:
- プロンプトを短くします。Prefill のコストはプロンプトのトークン数に比例し、システムプロンプトのコストはリクエストごとに発生します。
- プレフィックスキャッシュを有効にします。繰り返し使用するプレフィックスを毎回計算せず、1 回だけ計算できます。
--max-num-batched-tokensを増やし、1 ステップでより多くの Prefill 処理を実行できるようにします。- モデルを疑う前にキューを確認します。
vllm:num_requests_waitingが 0 より大きい場合、リクエストはまだ開始されていません。これは容量の問題です。
Tokens per second が低すぎる場合:
- 重みを量子化します。1 つの重みに必要なバイト数が少ないほど、トークンごとの読み取り量も少なくなります。
- 使用しているカードの公開メモリ帯域幅を上のグラフと比較し、上限にどの程度近いかを確認します。
--max-num-batched-tokensを小さくし、Prefill による Decode の中断頻度を下げます。- コンテキスト長を確認します。会話が数千トークンまで長くなると、各ステップでより大きな KV キャッシュを読み取る必要があります。
ここではランタイムも重要です。Ollama と vLLM では Prefill と Decode のスケジュール方法が異なるため、一方に有効な設定が他方では何も改善しないことがあります。まず両方のフェーズを計測してから、変更する項目を 1 つに絞ります。
FAQ
最初の token に数秒かかるのに、その後のストリーミングが速いのはなぜですか?
待ち時間は prefill、ストリーミングは decode によるものです。prefill では、出力が生成される前にプロンプト全体を compute bound な 1 回の処理で処理するため、コストはプロンプトの長さに応じて増加します。decode では 1 ステップごとに 1 token を出力します。その速度はメモリ帯域幅で決まり、プロンプトの長さにはほぼ左右されません。通常の原因は、すべてのリクエストに長い system prompt を付けていることです。Prefix caching を使うと、この繰り返し部分のコストをなくせます。
プロンプトが長いと、tokens per second は低下しますか?
少し低下します。ただし、理由は TTFT とは異なります。decode の各ステップでは、それまでのすべての token の keys と values を読み取るため、KV cache が大きいほど、1 token あたりの読み取りバイト数が増えます。一般的な 8 billion parameter の構成では、cache は 1 token あたり約 128 KiB です。そのため、8,000 token のコンテキストでは、各ステップでおよそ 1 GB を読み取ることになります。長いプロンプトによる影響は、ストリーミング速度よりも TTFT に現れる方が大きいです。
decode speed を予測できる GPU の仕様はどれですか?
メモリ帯域幅です。公表されている帯域幅をメモリ上の weights のサイズで割ると、1 stream における演算上の上限が得られます。compute 性能が高くても帯域幅が同じカードでは、ストリーミングは速くなりません。8 bits への量子化で decode speed がほぼ 2 倍になるのも同じ理由です。compute 性能を変えずに、1 token あたりの読み取りバイト数を半分にできるためです。
ユーザーを追加すると throughput は上がるのに、各ユーザーの体感は遅くなるのはなぜですか?
weights の 1 回の読み取りで、batch 内の各 sequence の token を処理できるため、batch size が増えると合計 tokens per second は上昇します。一方、各 token は共有ステップの完了を待つため、ユーザーごとの latency も同時に上昇します。集計された throughput ではなく p99 inter token latency を監視し、リクエストが実行中ではなくキューで待機していないかを vllm:num_requests_waiting で確認してください。