跳至主要內容
推論基礎設施

拆解一次 LLM 推論請求

追蹤請求的預填充、解碼、快取與串流流程,核對時間與記憶體成本。

作者
Huang Tzu LinFounder
發布日期
閱讀時間
約 18 分鐘
更新於
目錄
  1. 01本篇在學習路線中的位置
  2. 02開始之前:先備知識與學習成果
  3. 03你將建置的成品
  4. 04範圍與貫穿範例請求
  5. 05請求生命週期
  6. 1. 閘道器與請求正規化
  7. 2. 對話範本與分詞
  8. 3. 准入控制、路由與排隊
  9. 4. Prefill(預填充階段)
  10. 5. Decode、取樣與串流輸出
  11. 6. 完成與遙測
  12. 06注意力機制與 KV 快取
  13. 07可重現的 KV 記憶體公式
  14. 08指標定義
  15. 09完整範例:核對時間線
  16. 10建立指標計算器
  17. 11刻意破壞的案例
  18. 錯誤的時間分母
  19. 錯誤的架構假設
  20. 12動手實作
  21. 13可量測的完成標準
  22. 14提取練習
  23. 15解答
  24. 16本文不涵蓋的內容
  25. 17來源與延伸閱讀

一次 API 呼叫的背後,其實串起了一整條推論服務管線:請求驗證、分詞(tokenization)、准入控制、排程、模型執行、取樣、串流輸出與遙測紀錄。本篇教學針對採用完整因果注意力(full causal attention)的 decoder-only Transformer 逐步拆解這條管線。先界定這個架構範圍很關鍵,因為 multi-query attention(MQA)、grouped-query attention(GQA)、multi-head latent attention(MLA)、滑動視窗或稀疏注意力、混合狀態空間層(state-space layers),乃至推測式多 token 解碼(speculative decoding),都會改變下文部分記憶體或時間計算公式。

本篇不背誦某家供應商的延遲數字,而是從明確的假設出發,建立一條請求時間軸,讓 KV 記憶體、TTFT、TPOT、ITL、端對端延遲與 token 速率都能互相對得上。

互動實驗

讀懂請求時間軸

TPOT 為何除以輸出 token 數減一?

收到請求最後 token
等待首 token · ms
190
後續 tokens · ms
3,725
190 msTTFT
25 msTPOT
3,915 ms端對端
190 + (150 − 1) × 25 = 3,915 ms

第一個 token 結束 TTFT;只有後續輸出 token 形成 token 間隔。

讀懂目前結果

首 token 到達
190 ms
後續間隔數
150 − 1 = 149
最後 token 到達
3915 ms

只輸出一個 token 時沒有後續間隔:端對端時間等於 TTFT,TPOT 未定義。此測例的多 token 輸出恰有「輸出數−1」個間隔。

參數化時間測例,後續每 token 間隔固定為 25 ms。

本篇如何銜接前後內容

本篇在學習路線中的位置

帶入前作:00-1:五層行為分類表、00-2:請求封裝與 token 預算

本篇新增:請求時間軸與指標計算器。請將假設、來源 ID 與失敗案例連同成果一併留存。

後續用途:I-01、I-02 與 I-06 將沿用這些成果。跨主題系列的先備順序請見完整閱讀路線。文章 ID 是固定標籤,編號不代表閱讀順序。

學習目標與實作準備

開始之前:先備知識與學習成果

請先閱讀 0-1:大型語言模型到底在做什麼 與 0-2:提示、上下文視窗,以及你如何跟 LLM 對話。

讀完本篇後,你將能夠:

  1. 完整追蹤一筆推論請求從閘道器進入系統到串流回應結束的過程;

  2. 解釋正式環境的解碼器為何通常會在各個 decode 步驟之間保留 KV 快取;

  3. 依據注意力架構(而非總參數量)算出每個 token 的 KV 位元組數;

  4. 區分 TTFT、ITL、TPOT、端對端(E2E)延遲、單一使用者輸出速率、系統吞吐量與有效吞吐量(goodput);以及

  5. 抓出差一(off-by-one)的時間分母錯誤,並排除套錯注意力架構的 KV 公式。

你將建置的成品

你會實作出兩份可直接檢驗的產物:

  • request-timeline.csv:記錄單筆請求在用戶端可觀測到的各階段事件時間;以及

  • request-metrics.json:由小型 Python 計算器根據同一組假設自動算出的指標結果。

文中數值皆來自教學用的合成軌跡,並非 OptiVerse 或任何第三方部署環境的基準測試結果。

範圍與貫穿範例請求

旅行 Copilot 送出以下請求:

幫我找京都每晚 200 美元以下、可泡溫泉、輪椅可通行的飯店。

整份提示(prompt)由三個部分組成:

組成項目

Token 數

系統指令

200

檢索到的飯店證據

800

使用者查詢

25

輸入總計

1,025

模型架構假設如下:

模型屬性

數值

Transformer 層數

80

Query head 數

64

KV head 數

8(GQA)

Head 維度

128

KV 資料型別

BF16,2 bytes

可見輸出 token 數

150

這裡務必分清 query head 與 KV head 的數量。GQA 讓每 8 個 query head 共用 1 組 key/value head;要是誤把 query_heads=64 代入 KV 公式,快取大小會被高估 8 倍。推論工程指南同樣會分開討論 MHA、MQA 與 GQA,因為三者的 KV 記憶體占用量差異很大(NVIDIA 推論最佳化指南)。

請求生命週期

1. 閘道器與請求正規化

服務入口(serving edge)會先驗證呼叫者身分、套用速率與酬載大小上限、檢查模型名稱與取樣參數、指派 request ID,並記錄請求抵達的時間戳記。即使模型此刻還沒開始執行,這些前置處理耗費的時間都已經算進用戶端觀測到的 TTFT。

2. 對話範本與分詞

伺服器接著透過固定版本的對話範本(chat template)與分詞器(tokenizer)將 API 訊息展開成 token 序列。Token ID 雖然決定了文字序列本身,但若要正確重用 KV 快取,模型權重、轉接器(adapter)、位置、注意力遮罩與任何多模態輸入也必須一致。計費則依各家供應商定義的 token 計費類別,光憑 token ID 相同,既不能認定價格一致,也不能認定狀態可以重用。

3. 准入控制、路由與排隊

全域路由器會挑選相容的模型副本,再由推論引擎排程器檢查當下的 token 預算、序列槽位與 KV 記憶體餘裕,決定是否接納這筆請求。預算已滿時,請求就必須排隊等待,或由背壓(backpressure)機制直接拒絕;排隊等待的每一毫秒都會直接拉長 TTFT。

4. Prefill(預填充階段)

進入 prefill 階段後,模型會處理全部 1,025 個輸入 token,並在每個注意力層算出對應的 K 與 V 狀態。雖然各輸入位置可以平行處理,因果遮罩(causal mask)仍會維持自迴歸語意。本篇是純文字範例,沒有隱藏推理 token 或工具呼叫,因此最後一個輸入位置算出的 logits 就用來產生第一個可見輸出 token。其他生成模式(例如隱藏推理或工具呼叫)則可能在第一個可見輸出之前先做額外的工作。

Prefill 的算術強度(arithmetic intensity)通常較高,偏向受運算能力限制(compute-bound);小批次的 decode 算術強度通常較低,偏向受記憶體頻寬限制(memory-bandwidth-bound)。不過這只是 roofline 模型下的常見傾向,並非鐵律:輸入長度、批次大小、模型架構、kernel 實作與硬體,都可能讓任一階段跨過運算與頻寬的瓶頸邊界。

5. Decode、取樣與串流輸出

從第二個輸出 token 開始,推論引擎每一步都會餵入前一步選出的 token、讀取先前留存的 KV 狀態、算出新 logits、取樣或選出下一個 token,並將解碼出的文字片段串流回傳。完整因果注意力每一步都要讀取整段前綴,而前綴每步增加 1 個 token;滑動視窗注意力、稀疏注意力、循環層或混合層則會改變這種讀取模式。

如果模型在吐出最後一個輸出 token 後立刻結束生成,通常就不需要再為這個結尾 token 寫入下一步才會用到的 KV 狀態,因此請求結束瞬間的精確快取筆數會隨引擎實作而異。本篇範例計算的是:在處理完 149 個輸出 token、進而產生全部 150 個可見輸出 token 時的 KV 快取大小。

6. 完成與遙測

生成結束後,伺服器會送出用量統計與結束原因等中繼資料,釋放該請求獨占且不再供前綴重用的 KV 區塊,並寫入延遲、token 數、排隊時間、快取命中、錯誤與取消等遙測訊號。若用戶端中途斷線,服務層也應在引擎支援時及時取消請求,避免 GPU 繼續為無人接收的輸出白做 decode 運算。

注意力機制與 KV 快取

在每個 token 位置上,注意力層都會把隱藏狀態投影成 query、key 與 value 三組向量。在完整因果自注意力下,當前的 query 會與所有先前位置及自身位置的 key 計算關聯度,再以此對各位置的 value 加權求和,這正是原始 Transformer 論文提出的縮放點積注意力(scaled dot-product attention)Q/K/V 形式(Vaswani et al., 2017)。

要是沒有 KV 快取,後續每一個 decode 步驟都得把前面所有處理過的位置重新做一次 K/V 投影。將這些 K/V 張量留在快取裡,每一步就只需為剛餵入的新 token 計算投影狀態。不過別把兩段運算成本混為一談:快取省下的是先前位置的 K/V 投影重算,但因果注意力每一步仍得讀取整段快取前綴來計算注意力分數,這部分的成本依然會隨序列長度增加。

可重現的 KV 記憶體公式

在標準 MHA 或 GQA 資料布局中,每個 KV head 各存一組 K 張量與一組 V 張量:

plaintext
KV_bytes_per_token = 2 × layers × kv_heads × head_dim × bytes_per_element
KV_bytes(sequence)  = KV_bytes_per_token × sequence_tokens

代入本篇的 80 層 GQA 模型參數:

plaintext
KV bytes/token = 2 × 80 × 8 × 128 × 2
               = 327,680 bytes
               = 0.3125 MiB

Prompt KV       = 327,680 × 1,025
               = 335,872,000 bytes
               = 320.3125 MiB

KV after 149 processed output tokens
               = 327,680 × (1,025 + 149)
               = 384,696,320 bytes
               = 366.875 MiB

以上算出的是未分片狀態下的邏輯 K/V 資料量,不等於 GPU 實際占用的總記憶體。實體記憶體開銷還取決於 KV 資料型別、量化縮放因子(scales)、對齊填補與區塊進位、執行期工作空間(workspace),以及張量平行(tensor parallelism)如何切分或複製 KV head、管線平行(pipeline parallelism)如何把各層分到不同裝置。至於 MLA、滑動視窗、循環層或混合架構,因為留存的狀態結構不同,必須先推導各自的狀態大小,不能直接套用 kv_heads 公式。

指標定義

TTFT 是首個 token 延遲(Time to First Token),TPOT 是首個 token 之後的平均每輸出 token 耗時(Time per Output Token),ITL 是相鄰輸出之間的 token 間延遲(Inter-Token Latency),E2E 則是端對端延遲。在本範例中,令用戶端送出請求的時間為 s、收到第一個非空可見輸出的時間為 f、收到最後一個可見輸出的時間為 e、可見輸出 token 總數為 O:

plaintext
TTFT = f - s
E2E  = e - s
TPOT = (E2E - TTFT) / (O - 1), for O > 1

ITL 量測的是相鄰兩次串流輸出之間的每一個時間間隔,TPOT 則是將首個 token 之後的生成耗時濃縮成單筆請求的平均值。當每次串流事件剛好吐出 1 個 token 時,TPOT 在數學上就等於該筆請求所有 ITL 的算術平均數,但個別間隔仍可能大幅抖動、未必貼近平均值;若啟用推測式解碼,單次事件可能一次送出多個通過驗證的 token,ITL 與 TPOT 就會出現落差。vLLM 的基準測試文件因此特別提醒,對照結果時務必核對公式與量測點,不能只看指標名稱相同就直接比較(vLLM Benchmark CLI)。

以下兩種單筆請求的 token 速率,回答的是不同層面的問題:

plaintext
decode-only output rate = (O - 1) / (E2E - TTFT)
whole-request output rate = O / E2E

系統輸出吞吐量(system output throughput)是把計入的輸出 token 總數除以量測區間長度。合格的報告必須交代三件事:統計的是穩態區間內的 token 事件,還是已完成請求的封閉群組(closed cohort);跨越區間邊界的請求如何處理;失敗請求產生的 token 是否排除。有效吞吐量(goodput)則只計算單位時間內符合指定 SLO 的請求數。用不同計算規則得出的速率,彼此不能混用。

另外看一個獨立的 10 秒封閉群組區間範例:假設全部 100 筆送出的請求都在這段區間內結束,其中 98 筆成功、2 筆失敗,且有 95 筆成功請求符合延遲 SLO,沒有任何未完成或跨區間的請求:

plaintext
request throughput = 98 / 10 = 9.8 requests/s
observed goodput   = 95 / 10 = 9.5 requests/s
error rate         = 2 / 100 = 2%

完整範例:核對時間線

建立 request-timeline.csv:

plaintext
event,time_ms,detail
client_send,0,request leaves application
server_arrival,5,gateway has request
scheduler_admit,45,prefill begins
first_visible_token,190,token 1 of 150
last_visible_token,3915,token 150 of 150

這份端點 CSV 只記錄首尾兩個輸出事件,因此足以算出平均 TPOT,但看不出個別間隔的分布。為了讓教學軌跡能直接手算核對,這裡額外假設每次事件吐出 1 個可見 token、149 個間隔固定皆為 25 ms,且所有事件共用同一個合成參考時鐘(在真實環境中,若要拿用戶端與伺服器的時間戳記跨機相減,必須先完成時鐘同步)。此處將 E2E 定義到最後一個可見 token 抵達為止,不含後續才送達的結束中繼資料:

plaintext
last token time = 190 + 149 × 25
                = 3,915 ms

TTFT            = 190 - 0
                = 190 ms

E2E             = 3,915 - 0
                = 3,915 ms

TPOT            = (3,915 - 190) / (150 - 1)
                = 3,725 / 149
                = 25 ms/token

decode-only rate = 149 / 3.725
                 = 40.00 tokens/s

whole-request rate = 150 / 3.915
                   = 38.31 tokens/s

40.00 tokens/s 反映的是首個 token 出現後的純串流生成速度;38.31 tokens/s 則把網路傳輸、排隊等待與 prefill 的首個 token 延遲一併攤入。NVIDIA 的基準測試指南同樣將 TTFT、ITL/TPOT、單一使用者輸出速率與系統整體吞吐量拆開定義,不過它把 ITL 定義為相鄰 token 之間的平均間隔,並視為 TPOT 的別名(NVIDIA 基準測試定義)。

建立指標計算器

將以下程式存為 request_metrics.py,與 request-timeline.csv 放在同一個目錄,並執行 python request_metrics.py > request-metrics.json。這支程式會直接讀取端點時間戳記來計算指標,至於「每個間隔固定 25 ms」則是合成軌跡本身的額外教學假設:

python
import csv
import json
from pathlib import Path

layers, kv_heads, head_dim, bytes_per_element = 80, 8, 128, 2
input_tokens, output_tokens = 1025, 150
window_seconds, attempted_requests = 10, 100
successful_requests, slo_passing_requests = 98, 95

with Path("request-timeline.csv").open(newline="") as source:
    rows = list(csv.DictReader(source))
if len({row["event"] for row in rows}) != len(rows):
    raise ValueError("duplicate event")
times = {row["event"]: float(row["time_ms"]) for row in rows}
s, f, e = (times[key] for key in
           ("client_send", "first_visible_token", "last_visible_token"))
if not (s <= f <= e) or output_tokens < 1:
    raise ValueError("invalid visible-output timeline")
if not (0 <= slo_passing_requests <= successful_requests <= attempted_requests):
    raise ValueError("inconsistent request counts")
if window_seconds <= 0 or attempted_requests <= 0:
    raise ValueError("invalid system-window denominator")
gaps = output_tokens - 1
decode_ms = e - f
if gaps > 0 and decode_ms <= 0:
    raise ValueError("positive decode duration required")
kv_bytes_per_token = 2 * layers * kv_heads * head_dim * bytes_per_element
artifact = {
    "scope": "full causal GQA; no hidden output; logical unsharded KV",
    "kv_bytes_per_token": kv_bytes_per_token,
    "prompt_kv_mib": kv_bytes_per_token * input_tokens / 2**20,
    "stop_kv_mib": kv_bytes_per_token * (input_tokens + gaps) / 2**20,
    "ttft_ms": f - s,
    "e2e_ms": e - s,
    "tpot_ms": decode_ms / gaps if gaps else None,
    "decode_output_tokens_per_second": gaps / (decode_ms / 1000) if gaps else None,
    "whole_request_output_tokens_per_second": output_tokens / ((e-s)/1000) if e > s else None,
    "request_throughput_rps": successful_requests / window_seconds,
    "observed_goodput_rps": slo_passing_requests / window_seconds,
    "error_rate": (attempted_requests-successful_requests) / attempted_requests,
}
print(json.dumps(artifact, indent=2))

預期輸出數值:

plaintext
kv_bytes_per_token = 327680
prompt_kv_mib = 320.3125
stop_kv_mib = 366.875
e2e_ms = 3915
tpot_ms = 25
decode_output_tokens_per_second = 40
whole_request_output_tokens_per_second ≈ 38.31
request_throughput_rps = 9.8
observed_goodput_rps = 9.5
error_rate = 0.02

刻意破壞的案例

錯誤的時間分母

若誤寫成 190 + 150 × 25 = 3,940 ms,等於替 150 個可見 token 算了 150 個間隔,忘了第 1 個 token 在 TTFT = 190 ms 就已經抵達,後續其實只有 149 個間隔。反過來拿全部 150 個 token 去除以純 decode 時間,也會犯下對稱的錯誤,把 prefill 產生的首個 token 誤算到 decode 區段裡。

錯誤的架構假設

要是把 kv_heads=8 換成 query_heads=64,算出來會變成 2.5 MiB/token,是原本 GQA 快取的 8 倍;這個數字對應的是其餘條件相同的 MHA 模型,而非本篇的 GQA 模型。至於 MLA 或混合狀態空間架構,因為儲存的是壓縮潛在向量或循環狀態,無論代入哪一個 head 數都不正確,必須重新推導狀態公式。

動手實作

請針對以下另一組假設型模型與時間軌跡修改計算器:

  • 48 層 Transformer;

  • 32 個 query head 與 4 個 KV head;

  • head 維度 128;

  • FP8 KV 儲存格式,每個元素 1 byte(本練習暫時忽略縮放因子與對齊開銷);

  • 4,096 個輸入 token 與 256 個可見輸出 token;

  • TTFT 為 420 ms;以及

  • 固定 18 ms 的 token 間延遲。

輸出 request-metrics-alt.json,並回答以下問題:

  1. 每個 token 的 KV 大小是多少 MiB?

  2. 輸入提示的 KV 快取占多少 GiB?

  3. 端對端(E2E)延遲是多少?

  4. 純 decode 輸出速率與整筆請求輸出速率各是多少?

  5. 簡化版 FP8 公式省略了哪些實體記憶體成本?

預期核心結果:

plaintext
KV bytes/token = 2 × 48 × 4 × 128 × 1 = 49,152 bytes = 0.046875 MiB
prompt KV      = 192 MiB = 0.1875 GiB
E2E            = 420 + 255 × 18 = 5,010 ms
decode rate    = 255 / 4.590 ≈ 55.56 tokens/s
whole rate     = 256 / 5.010 ≈ 51.10 tokens/s

可量測的完成標準

本篇實作產物須滿足以下所有條件才算通過:

  • 明確列出架構範圍、KV 資料布局、資料型別與各項 token 數量;

  • 輸入提示的三個組成部分合計為 1,025 個 token;

  • KV 記憶體容量由 kv_heads 推導,而非總參數量或 query head 數;

  • 首尾可見 token 事件之間正好對應 149 個 ITL 間隔;

  • 最終時間戳記為 3,915 ms;

  • TTFT、E2E、TPOT、純 decode 速率與整筆請求速率皆能由 CSV 重新算出;

  • 請求吞吐量、觀測有效吞吐量(goodput)與錯誤率皆能由獨立的 10 秒系統量測區間重算出來;

  • 未將教學用合成數字描述成任何供應商的實測結果;

  • 能明確排除將 MHA 套入 GQA 的錯誤計算;以及

  • 延伸練習已把省略的量化縮放因子與對齊開銷,列為實體記憶體估算的限制。

提取練習

  1. 在模型真正開始執行之前,有哪些階段會計入用戶端觀測到的 TTFT?

  2. 相較於其他條件相同的 MHA 模型,為什麼 GQA 能減少 KV 記憶體占用?

  3. ITL 與 TPOT 有什麼差別?

  4. 為什麼 150 個可見 token 之間只有 149 個時間間隔?

  5. 為什麼同一條軌跡會算出 40.00 tokens/s 與 38.31 tokens/s 兩種不同速率?

  6. 在什麼情況下,標準的 kv_heads 公式不再適用?

解答

  1. 閘道器身分驗證與參數檢查、對話範本展開與分詞、路由選擇、准入控制,以及排程器佇列等待。

  2. 多個 query head 共用較少組 key/value head,因此每一層為每個 token 儲存的 K 與 V 向量數量更少。

  3. ITL 是連續兩次串流輸出之間的個別間隔;TPOT 則是首個 token 之後,整筆請求的平均每輸出 token 耗時。

  4. 第 1 個可見 token 出現即代表 TTFT 結束,從第 2 個到第 150 個 token 的轉換才會產生首個 token 之後的間隔(150 - 1 = 149)。

  5. 40.00 tokens/s 排除了 TTFT,只拿後續 149 個 token 除以純 decode 時間(3.725 s);38.31 tokens/s 則拿全部 150 個可見 token 除以整筆請求的端對端延遲(3.915 s)。

  6. 只要模型留存的狀態不是「每個 KV head、每個 token 各存一組 K 與 V 向量」(例如 MLA 或循環/混合架構),該公式就不適用。滑動視窗層則須改用引擎實際保留的長度;而即便邏輯公式仍成立,量化縮放因子、對齊與區塊進位也會帶來額外的實體記憶體開銷。

本文不涵蓋的內容

本篇只單獨追蹤一筆推論請求。I-01 會接著加入多請求並行時的 token 預算排程與過載處理;I-02 將單 token 的 KV 大小換算成分頁區塊容量;I-04 拆解單機前綴重用與分散式快取感知路由;I-03 評估在分離式 prefill 與 decode 資源池之間搬移 KV 狀態的取捨;I-06 則探討如何把密集模型分配到多張 GPU 上,以及這種配置帶來的通訊成本。


來源與延伸閱讀

  • Vaswani, A., Shazeer, N., Parmar, N., et al. "Attention Is All You Need." NeurIPS, 2017。Transformer 注意力機制與 Q/K/V 投影的第一手論文。arxiv.org/abs/1706.03762

  • Hugging Face. "Cache Strategies." Transformers 現行文件,查閱日期 2026-11-02。KV 快取行為與各類快取變體的官方說明。huggingface.co

  • vLLM Project. "Benchmark CLI." 現行文件,查閱日期 2026-11-02。TTFT、ITL 與 TPOT 量測點與公式的官方定義。docs.vllm.ai

  • NVIDIA. "LLM Inference Benchmarking: Fundamental Concepts." 2025-04。用戶端延遲指標與吞吐量分母定義的官方解析。developer.nvidia.com

  • NVIDIA. "Mastering LLM Techniques: Inference Optimization." 2023-11。Prefill/decode 階段、KV 快取、MQA/GQA 與推論記憶體的官方技術概覽。developer.nvidia.com

閱讀順序

完整閱讀路線

章節 18 / 25

你在這裡72%
  1. 01
  2. 02
  3. 03
  4. 04
  5. 05
  6. 06
  7. 07
  8. 08
  9. 09
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25

查看先備知識、分支路線與共用實作 →

分享這篇文章
XLinkedIn

Huang Tzu Lin

富宇資訊科技創辦人。撰寫 AI 系統設計、證據與工程取捨的文章。

透過 RSS 訂閱

使用慣用的 RSS 閱讀器追蹤最新的 AI 系統實作教學。

開啟 RSS Feed

支援任何 RSS 閱讀器,新文章將自動送達。

回到頂端