拆解一次 LLM 推論請求
追蹤請求的預填充、解碼、快取與串流流程,核對時間與記憶體成本。
- 作者
- Huang Tzu LinFounder
- 發布日期
- 閱讀時間
- 約 18 分鐘
- 更新於
一次 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 · ms
- 190
- 後續 tokens · ms
- 3,725
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 對話。
讀完本篇後,你將能夠:
完整追蹤一筆推論請求從閘道器進入系統到串流回應結束的過程;
解釋正式環境的解碼器為何通常會在各個 decode 步驟之間保留 KV 快取;
依據注意力架構(而非總參數量)算出每個 token 的 KV 位元組數;
區分 TTFT、ITL、TPOT、端對端(E2E)延遲、單一使用者輸出速率、系統吞吐量與有效吞吐量(goodput);以及
抓出差一(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 張量:
KV_bytes_per_token = 2 × layers × kv_heads × head_dim × bytes_per_element
KV_bytes(sequence) = KV_bytes_per_token × sequence_tokens代入本篇的 80 層 GQA 模型參數:
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:
TTFT = f - s
E2E = e - s
TPOT = (E2E - TTFT) / (O - 1), for O > 1ITL 量測的是相鄰兩次串流輸出之間的每一個時間間隔,TPOT 則是將首個 token 之後的生成耗時濃縮成單筆請求的平均值。當每次串流事件剛好吐出 1 個 token 時,TPOT 在數學上就等於該筆請求所有 ITL 的算術平均數,但個別間隔仍可能大幅抖動、未必貼近平均值;若啟用推測式解碼,單次事件可能一次送出多個通過驗證的 token,ITL 與 TPOT 就會出現落差。vLLM 的基準測試文件因此特別提醒,對照結果時務必核對公式與量測點,不能只看指標名稱相同就直接比較(vLLM Benchmark CLI)。
以下兩種單筆請求的 token 速率,回答的是不同層面的問題:
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,沒有任何未完成或跨區間的請求:
request throughput = 98 / 10 = 9.8 requests/s
observed goodput = 95 / 10 = 9.5 requests/s
error rate = 2 / 100 = 2%完整範例:核對時間線
建立 request-timeline.csv:
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 抵達為止,不含後續才送達的結束中繼資料:
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/s40.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」則是合成軌跡本身的額外教學假設:
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))預期輸出數值:
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,並回答以下問題:
每個 token 的 KV 大小是多少 MiB?
輸入提示的 KV 快取占多少 GiB?
端對端(E2E)延遲是多少?
純 decode 輸出速率與整筆請求輸出速率各是多少?
簡化版 FP8 公式省略了哪些實體記憶體成本?
預期核心結果:
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 的錯誤計算;以及
延伸練習已把省略的量化縮放因子與對齊開銷,列為實體記憶體估算的限制。
提取練習
在模型真正開始執行之前,有哪些階段會計入用戶端觀測到的 TTFT?
相較於其他條件相同的 MHA 模型,為什麼 GQA 能減少 KV 記憶體占用?
ITL 與 TPOT 有什麼差別?
為什麼 150 個可見 token 之間只有 149 個時間間隔?
為什麼同一條軌跡會算出 40.00 tokens/s 與 38.31 tokens/s 兩種不同速率?
在什麼情況下,標準的
kv_heads公式不再適用?
解答
閘道器身分驗證與參數檢查、對話範本展開與分詞、路由選擇、准入控制,以及排程器佇列等待。
多個 query head 共用較少組 key/value head,因此每一層為每個 token 儲存的 K 與 V 向量數量更少。
ITL 是連續兩次串流輸出之間的個別間隔;TPOT 則是首個 token 之後,整筆請求的平均每輸出 token 耗時。
第 1 個可見 token 出現即代表 TTFT 結束,從第 2 個到第 150 個 token 的轉換才會產生首個 token 之後的間隔(
150 - 1 = 149)。40.00 tokens/s 排除了 TTFT,只拿後續 149 個 token 除以純 decode 時間(
3.725 s);38.31 tokens/s 則拿全部 150 個可見 token 除以整筆請求的端對端延遲(3.915 s)。只要模型留存的狀態不是「每個 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
Huang Tzu Lin
富宇資訊科技創辦人。撰寫 AI 系統設計、證據與工程取捨的文章。
