プリフィルとデコード:生成の仕組み
Enterを押すと生成は、物理特性がまったく異なる2フェーズで走る。プリフィルはプロンプト全体を1回の並列パスで処理する——計算主体でトークンあたりは速いが、最初の単語が出る前にすべての完了を待つ。続くデコードは回答を1トークンずつ吐き出す——各ステップで実質すべてのモデル重みをメモリから読み直し、1トークンを生成する。「最初のトークンは遅く、その後ストリームする」は環境の癖ではなく、この2フェーズのことである。
フェーズ1:プリフィル——プロンプト
- すべてのプロンプトトークンが同時にモデルを通る。大規模な並列行列積——まさにGPUのために作られたもの。使用率は高く、トークン毎秒も高い。
- 体感としては初回トークンまでの時間(TTFT)。Enterを押した後の沈黙。
- このパスはまた、全プロンプトトークンのキー/バリューベクトルでKVキャッシュを埋める——デコードが読む控え帳。
- プロンプトが長いほどTTFTは線形に伸びる。巨大なシステムプロンプト、RAG文書、チャット履歴は、エンジンがプレフィックスをキャッシュしない限り毎回プリフィルされる。
フェーズ2:デコード——回答
- フォワードパス1回につきトークン1つ。そのパスには全重みが必要で、1トークンの生成はモデル全体のVRAMからのストリーミングを意味する。
- 上限は計算性能ではなくメモリ帯域幅。概ねデコードtok/s ≈ 実効帯域幅 ÷ 重みバイト数。約400 GB/s実効のカードで8 GBの重みなら上限は約50 tok/s。
- 量子化がデコードを速くする理由はこれ。重みあたりのビットが減れば、ステップごとのストリームバイト数が減る(速度を参照)。
非対称の理由:プリフィルは重みの1読み取りあたり数千トークンを処理する(巨大行列、演算強度が高い=計算律速)。デコードはきっちり1トークン(1トークンのために全重みバイトを取得=帯域律速)。同じ重み、正反対のボトルネック。
total_time ≈ prompt_tokens / prefill_tok_s + output_tokens / decode_tok_s
実例
プロンプト: 1,000トークン プリフィル 2,000 tok/s → 0.5 s (TTFT)
出力: 300トークン デコード 40 tok/s → 7.5 s
合計 ≈ 8.0 s — 実時間の94%がデコード
- 最初の0.5秒で1,000トークンを処理し、続く7.5秒で300トークンを生成。同じモデル、同じGPU、50倍の差——フェーズが違うのであって、頑張りが違うのではない。
- 40 tok/sは英語で毎秒約160文字(約4文字/トークン)。誰のタイピングより速く、ストリーミングが快適に読める理由(トークン)。
- 同じ40 tok/sでも韓国語では毎秒約40〜60文字。トークン速度は同一で、トークンあたりのテキストが少ない。
- 出力長はデコード側のダイヤル。max_tokensが生成の長さの上限を決め、ストップシーケンスが早期終了させる。どちらも実時間を線形に変える(エンジンパラメータ)。
- ストリーミングは知覚を変えるだけで物理は変えない。同じ40 tok/sのまま、完了を待つのではなくデコードが進むのを見る。
実例2:RAGのケース
プロンプト: 12,000トークン(システム+取得チャンク20個) プリフィル 1,500 tok/s → 8.0 s
出力: 500トークン デコード 40 tok/s → 12.5 s
合計 ≈ 20.5 s — 待ち時間の39%がTTFTだけ
これが検索主体のローカル構成の典型的な形。太ったプロンプトは、タイプ開始前に何秒もモデルが「考える」ことを意味する。効き目の順に対策。
- 取得を減らす——上位3〜5チャンクが通常、上位20に勝る(RAG)。
- システムプロンプトを固定し、エンジンがキャッシュ済みプレフィックスを再利用できるようにする。
- 履歴を刈る。古いターンは全文再生ではなく要約で足りる。
KVキャッシュ:成長する控え帳
キャッシュがなければ、トークンNの生成は過去N−1トークン全部についてアテンションをゼロから再計算することになる——毎ステップ同一の仕事をやり直し、総コストは長さの2乗。実際はこう動く。
- プリフィルが全プロンプトトークンのキーとバリューを計算し、一度だけ格納する。
- 各デコードステップは新トークンのクエリ/キー/バリューだけを計算し、そのK/Vをキャッシュに追加する。
- 新トークンのアテンションは、そのクエリと格納済み全キーの内積。
- 1トークンのデコードはこの順番。エンベッドし、キャッシュを読みながらレイヤーを走らせ、語彙へ射影し、サンプル——K/Vを追加し、繰り返す。
これにより各デコードステップは、コンテキスト長について2乗(全再計算)ではなく線形(キャッシュ読み取り)になる。代償はキャッシュ自体で、トークンごとに成長する。
KV bytes ≈ 2 × layers × kv_heads × head_dim × bytes_per_value × context
8B級モデル(32レイヤー、8 KVヘッド、ヘッド次元128、FP16):
2 × 32 × 8 × 128 × 2 B ≈ トークンあたり128 KiB
→ 20kコンテキストで約2.6 GB、128kコンテキストで約17 GB
このキャッシュはVRAMを重みと奪い合う——同じモデルを短いプロンプトで問題なく動かすマシンが、長文コンテキストのチャットで落ちる(KVキャッシュ、VRAM計算)。
バッチ処理が経済を変える
1トークンのために8 GBの重みを読むのは無駄が多く、同じ読み取りで多数のシーケンスを同時にさばける。8リクエストをバッチ化すれば1回の重みストリームで8トークンを得られ、スループットは計算資源が飽和するまでほぼ線形で伸びる。これがサービングの唯一のトリック(サービングAPI、同時実行)。ノートPCで単独使用ならトークンごとに全額を払い、サーバは同時ユーザー間で償却する。
メーターの読み方
| メーター | フェーズ | 主なドライバ |
|---|---|---|
| TTFT | プリフィル | プロンプト長、計算性能、プレフィックスキャッシュヒット |
| プロンプト評価tok/s | プリフィル | 計算性能、バッチサイズ |
| 生成tok/s | デコード | メモリ帯域幅、重みバイト数、バッチ |
| スループット(req/s) | 両方 | バッチサイズ、プレフィックスキャッシュ再利用、並列スロット |
| コンテキストメモリ | 両方 | トークンあたりKVバイト数、キャッシュ量子化 |
チューニング前に測ること。まともなエンジンはリクエストごとにプロンプト評価速度と生成速度をログに出す。1分のログでどのフェーズがレイテンシを食っているか分かる。当てずっぽうでは分からない。
チューニングはフェーズに従う。短いプロンプトとプレフィックスキャッシュがTTFTを直し、量子化と小さい重みがデコードを直す(エンジンパラメータ、トラブルシューティング)。