SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor

Prefill 與 decode:為什麼第一個 token 比較慢

Prefill 受計算能力限制,決定 time to first token;decode 受記憶體頻寬限制,決定每秒 token 數。請在自己的 GPU 上分開測量。

Prefill 與 decode:用一段話說明

Prefill 與 decode 是理解自架 LLM(large language model)延遲問題時最重要的區分。Prefill 會在單次處理中讀取完整 prompt,瓶頸在計算能力。Decode 會一次寫入一個 token 來產生答案,瓶頸在記憶體頻寬。首個 token 的產生時間屬於 prefill 指標。每秒 token 數則屬於 decode 指標。

兩個階段都在同一個 GPU(graphics processing unit)上執行,使用相同的權重,且位於同一個 process 中,因此很容易將它們視為同一項工作。但它們的行為就像兩個共用同一個裝置的不同程式。將兩者分開分析後,許多原本令人困惑的結果就不再難以理解。

為什麼 prefill 受計算能力限制?

Prefill 會讓完整 prompt 依序通過每一層。2,000 token 的 prompt 會讓每次矩陣乘法都產生 2,000 列工作,因此 GPU 載入每個位元組的權重時,都需要執行大量算術運算。每移動一個位元組所執行的算術運算量稱為算術強度,而 prefill 的算術強度很高。裝置會接近計算能力上限,但記憶體匯流排仍有餘裕。

Prefill 會產生兩項結果:每個 prompt token 的 KV cache(key 與 value 張量),以及第一個輸出 token。在這次處理完成前,任何內容都不會傳送給讀取端。因此,prefill 時間與第一個 token 的時間(TTFT)通常接近相同。

Prefill 成本會隨 prompt 長度增加。線性部分是每層的矩陣運算。二次部分是 attention,因為每個 token 都會注意前面的每個 token;在 context 很長時,這部分才會開始顯著增加。因此,prompt 加倍時,TTFT 至少也會加倍。

你可以在 1 分鐘內觀察到這個現象。先向伺服器傳送 200 token 的 prompt,再傳送 2,000 token 的 prompt,每次都要求產生相同數量的輸出 token。TTFT 會明顯上升。產生第一個 token 後的串流速度幾乎不變。

為什麼 decode 受記憶體頻寬限制?

Decode 每個步驟只產生 1 個 token。為了產生這 1 個 token,GPU 必須從記憶體讀取模型中的每個權重,讓每個權重參與幾次運算後丟棄。Arithmetic intensity 接近 1,因此運算單元大多時間都在等待。

Decode 速度慢,是因為每個 token 都需要從記憶體讀取整個模型。因此,記憶體匯流排決定處理速度,運算單元則處於閒置狀態。

因此,單一串流的 decode 速度上限,可以透過紙筆運算得出。將記憶體頻寬除以權重所占用的位元組數即可。

ChartPublished memory bandwidth and the decode ceiling it implies for a 16 GB model
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 bit 精度儲存的 8 billion parameter 模型大小。這是算術推導,不是 benchmark 結果。實測速率會低於此上限,而了解實測值低了多少很有用,因為這能告訴你應該調整 serving stack,還是升級硬體。

依序閱讀 6 列,模式很清楚。使用 dual channel DDR5 的 CPU 約可達 90 GB/s,因此該模型的 decode 上限約為每秒 6 個 token。L40S 約為 54。H100 SXM 公布的頻寬為 3350 GB/s,其上限約為每秒 209 個 token。

這也是 quantization 對 decode 速度最有效的單一調整因素。將相同模型從 16 bit 改以 8 bit 儲存後,每個 token 需要讀取的位元組數會減半,因此上限大致會加倍。你沒有增加任何運算,只是搬移了較少的記憶體資料。

如何在自己的伺服器上測量各階段?

Ollama 會在回應本文中提供拆分結果。要求非串流完成回應,然後讀取計數器。

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}'

使用你確實已下載的模型標籤,ollama list 會列出該標籤。prompt_eval_countprompt_eval_duration 是 prefill:提示詞 token 數量及其處理時間。eval_counteval_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 即可移除連線建立成本。執行兩次並保留第二次結果,因為第一次呼叫可能包含模型冷啟動載入時間。

vLLM 也會在 /metrics 發布 Prometheus 指標。執行 curl -s http://localhost:8000/metrics | grep -E 'time_to_first_token|inter_token_latency' 即可取得 vllm:time_to_first_token_secondsvllm:inter_token_latency_seconds 這兩個直方圖。加入 vllm:num_requests_runningvllm:num_requests_waiting 可查看佇列深度,加入 vllm:kv_cache_usage_perc 可查看快取壓力。這 5 個名稱就是完整的儀表板。

在負載下,vllm bench serve --model <name> --num-prompts 200 --request-rate 4 會驅動執行中的伺服器,並以百分位數回報第一個 token 的時間及每個輸出 token 的延遲。這是查看兩個階段彼此競爭的唯一方法。在調整任何設定前,先建立乾淨的基準:測量本機 LLM 每秒 token 數中的方法可提供在重新開機後仍可重現的基準。

為什麼長 system prompt 會延遲第一個 token,卻不會降低串流速度?

因為 system prompt 只會增加 prefill 工作。它會與提示中的其他內容在同一個階段處理,並在第一個 token 出現前完成。處理完成後,內容只會以 KV cache 項目的形式存在;decode 會與其他內容一併讀取這些項目。因此,3,000 token 的 system prompt 每次請求都會增加 TTFT,但幾乎不會改變每秒 token 數。

但這並非完全不變。這些額外的 KV 項目會在每個 decode 步驟重新讀取,因此過長的 prompt 確實會稍微降低 decode 速度。下一節將說明這一點。

解決方法是停止重複計算相同的前綴。支援 prefix caching 的伺服器會保留共用前綴的 KV cache 並重複使用,因此第二個帶有相同 system prompt 的請求可以完全略過該部分的 prefill。vLLM 將此功能稱為 automatic prefix caching;請查看你所用版本的 vllm serve --help,因為預設設定曾隨版本發布而變更。這個位於 GPU 中的 KV cache,與 API provider 向你計費的 prompt cache 並不是同一項功能;在調整任一項設定前,建議先閱讀KV cache 與 prompt cache 的差異

為什麼 context 填滿後,decode 會變慢?

原因有兩個,都是 KV cache 造成的。

第一個原因是頻寬。在每個 decode step,attention 都會讀取先前每個 token 的 key 與 value。Weights 是每個 token 的固定成本;KV cache 則會持續增長。你可以根據模型的 config.json 計算其大小:每個 token 的位元組數等於 2 乘以 num_hidden_layers、乘以 num_key_value_heads、乘以 head dimension(hidden_size 除以 num_attention_heads),再乘以每個元素的位元組數。開頭的 2 代表一個 key 和一個 value。

以常見的 8 billion parameter 配置為例,包含 32 個 layer、GQA(grouped query attention)下的 8 個 key 和 value head、head dimension 128,以及 16 bit precision,計算結果為 2 x 32 x 8 x 128 x 2 = 131,072 bytes,約為每個 token 128 KiB。因此,8,000 個 token 的對話,每個 request 大約會占用 1 GB 的 KV cache。

第二個原因是容量。這 1 GB 的記憶體無法再用來存放 weights 或其他使用者的 context。伺服器會在啟動時設定 KV pool 的大小;在 vLLM 中,這項設定透過 --gpu-memory-utilization 完成。Pool 填滿後,新的 request 就會等待。vllm:num_requests_waiting 持續上升,而 vllm:kv_cache_usage_perc 維持接近 1,就是這種狀態的明確特徵。部分 stack 會暫停正在執行的 request,之後再重新計算其 cache,而不是將 request 排入佇列。使用者會感覺串流輸出在中途停頓。

長 context 會產生兩次成本:開始時需要更多 prefill 工作,之後每個 token 都需要讀取更多記憶體。

為什麼批次處理能提高吞吐量,卻會增加尾端延遲?

因為 decode 受頻寬限制,從計算資源的角度來看,增加請求的額外成本很低。讀取一次權重,就能為批次中的每個序列產生一個 token。因此,總吞吐量會隨批次大小幾乎線性增加,直到 KV pool 用盡,或批次大到再次受計算能力限制為止。Continuous batching 會在每個 step 重新組成批次,因此完成的請求可以離開,佇列中的請求也能加入,不必等待其他請求完成。

代價會反映在百分位數上。每位使用者的下一個 token 現在都必須等待共用 step 中最慢的部分,因此 p50,也就是中位數,仍可維持在可接受範圍,但 p99,也就是 100 個請求中最慢的 1 個,會明顯延長。使用者最容易注意到 p99,因為那代表句子中途出現的停頓。

Prefill 會讓這個問題更加明顯。大型 prompt 在串流期間抵達時,會占用裝置完成一個很長的 step,所有正在串流的請求都會出現間隔。Chunked prefill 會將長 prompt 切成多個片段,再把各片段分別混入 decode batches,因而消除大部分影響。截至 August 2026,vLLM V1 engine 預設會採用這項機制,並透過 --max-num-batched-tokens 提供平衡設定。vLLM tuning documentation 明確說明這項取捨:較小的值,例如 2048,能提供較佳的 inter token latency (ITL),因為較少 prefill 會中斷 decode;較大的值則能提供較佳的 TTFT,因為單一批次可容納更多 prefill tokens。這個單一 flag 將 prefill 與 decode 之間的取捨,以可調整的數值呈現。p99 何時開始超出可接受範圍,取決於容量;單一自架 LLM 能服務多少並行使用者也會使用相同的指標分析這個問題。

為什麼更大的 GPU 有時完全沒有改善?

因為更大通常代表更多運算能力,而 decode 並不需要更多運算能力。

比較上方圖表中的兩列。A100 80GB 的公布頻寬為 2039 GB/s,L40S 則為 864 GB/s;decode ceiling 也完全相同地呈現差異:前者為每秒 127 tokens,後者為 54。以大多數指標來看,RTX 4090 是非常快的顯示卡,但其 1008 GB/s 頻寬,使 decode ceiling 為 63。兩張卡的其他規格可能不同,但單一串流的 decode 速度會跟著規格表上的頻寬走。

因此,加快 decode 有兩種方式:減少每個 token 需要讀取的位元組數量(將權重量化,或執行較小的模型),或購買頻寬更高的 GPU。Prefill 則相反。Prefill 需要運算能力,因此更快的顯示卡確實能縮短長提示的 TTFT。如果問題是產生第一個 token 需要 4 秒,更好的硬體可能有效。如果問題是文字輸出速度很慢,則可能沒有幫助。

是否應在不同 worker 上執行 prefill 與 decode?

大型 serving stack 會採用這種架構,稱為 prefill 與 decode 解耦。第一組 worker 只執行 prefill,第二組 worker 只執行 decode。第一組建立的 KV cache 會透過高速互連傳送給第二組。這是因為兩個階段需要不同的硬體與排程方式。Prefill 需要運算能力與大型 token 批次。Decode 需要記憶體頻寬與大量並行序列。分開後,每組 worker 都能獨立擴展,也能避免單一超大型 prompt 使所有進行中的串流停滯。

在只有 1 個 GPU 的單一 VPS(virtual private server)上,幾乎不值得這麼做。這等於讓單一裝置與自身分工,並將原本的指標傳遞變成傳輸數 GB cache 的網路操作。當你有足夠的加速器,能將整台機器分別配置給每個階段,且有足夠穩定的流量讓兩組 worker 持續運作時,這項技術才有實際效益。在此以下,使用 1 個 flag 的 chunked prefill 已能提供大部分相同的隔離效果。

數值不理想時應調整的項目

TTFT 過高時:

  • 縮短提示。Prefill 成本會隨提示 token 數量增加,而 system prompt 會在每次請求中重複計費。
  • 啟用 prefix caching,讓重複的前綴只計算一次,而不是每次都重新計算。
  • 提高 --max-num-batched-tokens,讓每個 step 能處理更多 prefill 工作。
  • 先檢查佇列,再判斷是否為模型問題。vllm:num_requests_waiting 大於 0 表示請求尚未開始,這是容量問題。

每秒 token 數過低時:

  • 將權重量化。每個權重的位元組數越少,每個 token 需要讀取的資料就越少。
  • 對照上方圖表檢查顯示卡公開的記憶體頻寬,確認目前數值距離上限有多近。
  • 降低 --max-num-batched-tokens,讓 prefill 較少中斷 decode。
  • 檢查 context length。對話累積到數千個 token 後,每個 step 都必須讀取大得多的 KV cache。

Runtime 也會影響結果,因為 Ollama 與 vLLM 排程 prefill 和 decode 的方式不同;某項設定對其中一者有效,對另一者可能完全沒有作用。先在兩個階段進行測量,再一次只調整一項設定。

FAQ

為什麼第一個 token 需要數秒,但後續 token 卻能快速串流?

等待時間來自 prefill,串流處理則來自 decode。prefill 會在產生任何輸出前,以一次受計算能力限制的處理階段完成整個 prompt,因此成本會隨 prompt 長度增加。接著,decode 每個步驟產生一個 token,速度由記憶體頻寬決定,幾乎不受 prompt 長度影響。每次請求都包含很長的 system prompt,通常就是原因。Prefix caching 可移除這部分重複成本。

Prompt 變長會降低每秒 token 數嗎?

會稍微降低,但原因與 TTFT 不同。每個 decode 步驟都會讀取所有先前 token 的 key 和 value,因此 KV cache 越大,每個 token 需要讀取的位元組就越多。以常見的 8 billion parameter 配置為例,cache 每個 token 約占 128 KiB,因此 8,000 token 的 context 約為 1 GB,而且每個步驟都會存取這些資料。長 prompt 造成的主要影響仍在 TTFT,而不是串流速度。

哪項 GPU 規格最能預測 decode 速度?

記憶體頻寬。將產品公布的頻寬除以記憶體中權重的大小,即可得到單一串流的理論運算上限。計算能力較高但頻寬相同的顯示卡,不會有更快的串流速度。這也是將模型量化為 8 bits 後 decode 速度約可提升一倍的原因:每個 token 需要讀取的位元組減半,但計算量不變。

為什麼增加使用者後 throughput 會提高,但每位使用者都覺得變慢?

一次讀取權重,就能為 batch 中的每個序列各處理一個 token,因此總 tokens per second 會隨 batch size 增加。此時每個 token 都必須等待共用的處理步驟,所以單一使用者的延遲也會同時上升。請監看 p99 inter token latency,不要只看 aggregate throughput 數值,並檢查 vllm:num_requests_waiting,以確認請求是在排隊,而不是正在執行。