預填與解碼:生成如何運作
按下 Enter 之後,生成分為兩個物理特性完全不同的階段。預填(prefill)用一次平行運算處理你整個提示詞——運算密集、單位 token 很快,但第一個字出現前你得等它全部跑完。接著解碼(decode)一次吐出一個 token——每一步都要從記憶體重新讀取幾乎全部的模型權重,只為了產出一個 token。「第一個 token 慢、後面才開始串流」不是你的環境有怪癖;這就是這兩個階段。
第一階段:預填——提示詞
- 所有提示詞 token 同時通過模型:大型平行矩陣乘法,正是 GPU 為之而生的運算。使用率高;每秒 token 數也高。
- 你感受到的是首 token 時間(TTFT):按下 Enter 後的那段沉默。
- 這一趟也把每個提示詞 token 的鍵/值向量填進 KV 快取——也就是解碼階段要讀的草稿本。
- 提示詞越長,TTFT 線性越長:大型系統提示詞、RAG 文件與對話歷史,除非引擎有快取前綴,每次都要重新預填。
第二階段:解碼——回答
- 每次前向計算產出一個 token。那次計算需要所有權重;產生單一 token 意味著把整個模型從 VRAM 串流讀一遍。
- 上限是記憶體頻寬而非算力:大致上解碼 tok/s ≈ 有效頻寬 ÷ 權重位元組數。8 GB 權重在有效頻寬約 400 GB/s 的卡上 ≈ 50 tok/s 的上限。
- 這就是量化能加速解碼的原因:每個權重的位元數越少,每一步要串流的位元組就越少(見 速度)。
為什麼有這種不對稱:預填每次讀權重能處理數千個 token(巨大矩陣、算術強度高——運算受限);解碼恰好只處理一個(每個位元組的權重只為單一 token 而取——頻寬受限)。同樣的權重,相反的瓶頸。
total_time ≈ prompt_tokens / prefill_tok_s + output_tokens / decode_tok_s
實例
prompt: 1,000 tokens prefill 2,000 tok/s → 0.5 s (TTFT)
output: 300 tokens decode 40 tok/s → 7.5 s
total ≈ 8.0 s — 94% of wall-clock is decode
- 前半秒處理了 1,000 個 token;接下來 7.5 秒產出 300 個。同一個模型、同一張 GPU、50 倍的差距——是階段不同,不是努力不同。
- 40 tok/s 約等於每秒 160 個英文字元(≈4 字元/token):比任何人的打字速度都快,這就是串流讀起來很順的原因(token 與分詞器)。
- 同樣的 40 tok/s 換成韓文大約是每秒 40–60 字元:token 速率相同,每個 token 的文字更少。
- 輸出長度是解碼端的旋鈕:max_tokens 限制生成長度,停止序列可以提早結束。兩者都線性改變實際耗時(引擎參數)。
- 串流改變的是感受,不是物理:你是在看著解碼發生,而不是在等它,速度仍是同樣的 40 tok/s。
實例二:RAG 場景
prompt: 12,000 tokens (system + 20 retrieved chunks) prefill 1,500 tok/s → 8.0 s
output: 500 tokens decode 40 tok/s → 12.5 s
total ≈ 20.5 s — TTFT alone is 39% of the wait
這就是重度檢索的本機環境的樣貌:肥大的提示詞意味著模型在開始打字前要「想」個幾秒。解法按效果排序:
- 少檢索一點——前 3–5 塊通常勝過前 20 塊(RAG)。
- 保持系統提示詞穩定,讓引擎能重用快取的前綴。
- 修剪歷史:舊輪次可以改成摘要,而不是完整重播。
KV 快取:不斷長大的草稿本
沒有快取的話,產生第 N 個 token 就要從頭對之前 N−1 個 token 重算注意力——每一步都在重做相同的工作,總成本與長度成平方關係。實際的做法:
- 預填為所有提示詞 token 計算鍵與值,並只存一次。
- 每個解碼步驟只計算新 token 的查詢/鍵/值,並把它的 K/V 附加到快取。
- 新 token 的注意力,就是它的查詢與所有已存鍵的內積。
- 解碼一個 token 的順序:嵌入、跑各層並讀取快取、投影到詞彙表、取樣——附加它的 K/V、重複。
現在每個解碼步驟的成本與上下文長度成線性(讀快取)而不是平方(全部重算)。代價是快取本身,它隨每個 token 增長:
KV bytes ≈ 2 × layers × kv_heads × head_dim × bytes_per_value × context
8B-class model (32 layers, 8 KV heads, 128 head dim, FP16):
2 × 32 × 8 × 128 × 2 B ≈ 128 KiB per token
→ ~2.6 GB at 20k context, ~17 GB at 128k context
這份快取會與權重爭搶 VRAM——長上下文對話能讓短提示詞時跑得好好的同一台機器當掉(KV 快取、VRAM 用量計算)。
批次處理改變經濟學
讀 8 GB 的權重只為吐出一個 token 很浪費;同樣的讀取可以一次服務多個序列。把 8 個請求打包成批次,一次權重串流就能產出 8 個 token——吞吐量近乎線性上升,直到算力飽和。這就是服務部署背後的全部技巧(端點服務、並行處理)。獨自在筆電上,你為每個 token 付全額;伺服器把它分攤給同時上線的多個使用者。
解讀儀表
| 儀表 | 階段 | 主要驅動因素 |
|---|---|---|
| TTFT | 預填 | 提示詞長度、算力、前綴快取命中 |
| 提示詞評估 tok/s | 預填 | 算力、批次大小 |
| 生成 tok/s | 解碼 | 記憶體頻寬、權重位元組數、批次 |
| 吞吐量(req/s) | 兩者 | 批次大小、前綴快取重用、平行插槽 |
| 上下文記憶體 | 兩者 | 每 token 的 KV 位元組數、快取量化 |
調校前先測量:每個認真的引擎都會在日誌記錄每個請求的提示詞評估與生成速度。一分鐘的日誌就能告訴你哪個階段吃掉了延遲;用猜的沒有用。
調校要對症下藥:更短的提示詞與前綴快取治 TTFT;量化與更小的權重治解碼(引擎參數、疑難排解)。
相關章節: 速度 · KV 快取 · 並行處理 · token 與分詞器