為何服務本身就是一門學問
你在概念層次上已經理解 訓練與推論 的差別:訓練調整權重,推論只跑前向傳播。因此很容易以為推論是簡單的那一半。對 大型語言模型(LLM)而言,事實恰好相反。一次訓練是攤提到數個月的一次性成本;推論卻是你在每個請求的每個 token、永遠都要付出的成本。壓低這個成本,正是今日應用人工智慧工程的一大部分,它有自己的詞彙、自己的瓶頸,以及自己出人意料的失敗模式。
LLM 推論之所以困難,並非因為算力不足——現代 GPU 的算術吞吐量極為龐大。困難在於自迴歸生成是循序的,而且受記憶體限制。要產生第 200 個 token,你必須先產生第 199 個,而且每一步都為了極少量的計算而讀取大量的狀態。本篇建立的心智模型,是本軌道其餘篇章所仰賴的基礎。
Prefill 與 decode:兩種非常不同的工作
從 Transformer 生成可分成兩個階段。Prefill(預填) 一次處理整段提示:每個提示 token 都注意它前面所有的 token,因此整段提示在一次高度平行、以矩陣運算為主的傳遞中流過網路。Decode(解碼) 接著透過 自迴歸解碼 一次吐出一個輸出 token:每個新 token 都需要對單一位置做一次前向傳播,並回頭注意先前所有的 token。
自回归生成循环示意图,每个输出 token 被反馈作为下一步的输入。
KV 快取,以及 decode 為何讀取如此之多
若天真地生成第 t 個 token,會重新計算前面 *t-1* 個 token 的鍵與值再做注意,使生成變成平方級。我們改為把每一層的鍵與值存進 KV 快取 並重複使用:在第 t 步只計算新 token 的查詢(query)、鍵(key)、值(value),把後兩者附加到快取再做注意。這把每步的計算從平方級降為線性——卻把問題轉移到記憶體。快取隨序列長度增長,而且每一個 decode 步都得整份讀取。
注意力作为软查询—键—值查找的示意图,带 softmax 权重。
# Decode step, conceptually q, k, v = project(x_t) # one token's worth kv_cache.k.append(k) # grow the cache kv_cache.v.append(v) attn = softmax(q @ kv_cache.k.T) @ kv_cache.v # read the WHOLE cache x_next = mlp(attn)
記憶體頻寬之牆
一個有用的數字是算術強度(arithmetic intensity):每從記憶體讀取一個位元組所執行的浮點運算數。Decode 的算術強度極低——批次大小為一時,你讀取數十億位元組的權重卻只做數十億次 FLOP,於是運算遠在資料抵達之前就算完了。這個核心是 受記憶體頻寬限制(memory-bandwidth-bound) 的,GPU 的張量核心多半閒置。形式化的圖像見 算術強度。
算术强度(每搬运一字节对应的浮点运算数)是解释解码瓶颈的唯一关键数字。
這一個事實就解釋了整個領域。批次處理把每次權重讀取攤提到許多請求上,提高算術強度。KV 快取壓縮與分頁減少你必須搬動的位元組。推測解碼在每一趟記憶體存取中做更多有用的運算。量化讓每個位元組承載更多資訊。把這道牆放在心上,之後每一招的動機就一目了然。
一切優化所追求的指標
服務有兩個延遲指標,由 TTFT 與 TPOT 刻畫。首 token 時間(time to first token, TTFT) 主要受 prefill 主導,量測使用者在任何東西出現前要等多久。每輸出 token 時間(time per output token, TPOT),有時稱為 token 間延遲,量測串流開始後穩態的 decode 速度。這些是面向使用者的服務水準目標(SLO),且與吞吐量相互權衡。
端到端延迟可分解为由预填充主导的 TTFT,加上每个后续生成 token 的一次 TPOT。
永恆的張力是 延遲與吞吐量。較大的批次更有效率地使用 GPU,提高所有使用者的每秒 token 數(吞吐量,這驅動了 推論成本),卻可能損害單一使用者的 TPOT。一個服務系統本質上就是個排程器,要在達成延遲 SLO 的同時,讓昂貴的硬體保持忙碌。