總覽與定位
為什麼 LLM 特別難 serve:自回歸、串流、GPU 記憶體與延遲/吞吐的交錯約束。
RAG 與 Agent 討論的是「怎麼組上下文、怎麼呼叫模型」;本篇切到純後端 infra:模型權重已就緒,你要把它變成可水平擴充、可觀測、可對 SLA 負責的 Inference Serving。不談訓練、微調或資料集——只談請求進來之後,GPU 上發生什麼、哪裡會爆。
建 KV · 定 TTFT
串流回客戶端
佔 GPU HBM
抬吞吐、管排隊
看第一個盒與第三個盒的關係:Prefill 決定 TTFT,但它寫出的 KV Cache 會一路佔著顯存直到請求結束——所以「首 token 快不快」與「這台卡還能接幾個人」是同一段計算的兩個面向。
建議先讀過 RAG 架構 再進來(軟依賴:理解「長 prompt 從哪來」)。本篇不重複檢索/chunking;也不談 Agent loop 或 tool calling——那些是編排層。這裡只回答:同一個模型權重,如何在 GPU 上穩定地服務多用戶推論。
Prefill、Decode 與 Token Streaming
一次請求在時間軸上拆成兩段計算型態,串流只是 Decode 階段對外的呈現。
- 一次處理完整 prompt(含 system/RAG context)
- 為每個 transformer 層寫入初始 KV Cache
- 通常可用高度並行的矩陣運算吃滿 GPU
- 長度一拉長(長 context),TTFT 幾乎線性惡化
- 每步只產出約 1 個 token,再 append 進 KV
- 算力密度低:瓶頸常是讀取巨大 KV/權重頻寬
- 透過 SSE/WebSocket/gRPC stream 推給客戶端
- 結束條件:EOS、max_tokens、或客戶端取消
產品面的「打字機效果」來自 Decode 串流;Infra 面則必須分開優化兩段:Prefill 要降 TTFT,Decode 要抬 TPOT(Time Per Output Token)與整體 tokens/s。把兩者綁在同一個同步 batch 裡,往往會讓長 prompt 請求拖死短請求的首 token。
看第三欄與第四欄的落差:KV 從 prefill 那一刻就滿載寫入,客戶端卻要到 decode 第 1 步才看到第一個字——這一格的時間差就是 TTFT,也是本篇所有排程取捨的起點。
面試或監控若只報一個 p99 latency,會掩蓋真相:使用者真正在意的是 TTFT 與後續 token 節奏。長答案請求的 wall-clock 幾乎必然高,不代表服務壞了。
Continuous Batching:把 GPU 從「等人」變成「一直算」
靜態 batch 等齊才跑;連續批處理讓請求動態進出 iteration,抬高吞吐。
傳統離線推論習慣「湊滿 B 筆再跑」:對 LLM 幾乎不可用——有人要 8 個 token、有人要 800 個,齊步走會讓短請求乾等、長請求佔滿 slot。現代 serving runtime(vLLM、TGI、TensorRT-LLM 等)的核心是 Continuous Batching(又稱 iteration-level scheduling):
- 每個 decode step 組成一個「當前活躍集合」
- 請求可在 step 邊界 加入(新 prefill 完成)或 離開(EOS/取消)
- GPU 幾乎每一步都在算,而不是等最慢那筆結束
| 策略 | 排程粒度 | 短請求 TTFT | GPU 利用率 | 實作複雜度 |
|---|---|---|---|---|
| 靜態 Batch | 整批起迄對齊 | 差(等人) | 低~中 | 低 |
| Continuous Batching | 每個 decode step | 較穩 | 高 | 中~高 |
| Prefill/Decode 分離佇列 | 兩類 worker/優先權 | 可調優 | 高(需配額) | 高 |
Batching 不是越大越好:batch 越大吞吐通常越高,但單請求的 TTFT/TPOT 會被同批鄰居拖累。
權重載入與 kernel launch 成本被多請求攤提;Decode 又偏 memory-bound,多序列一起算能更有效吃滿記憶體頻寬。代價是排程、分頁 KV、與公平性(長請求饿死短請求)都變難。
KV Cache 與 GPU 記憶體瓶頸
顯存同時要扛權重、啟動配置與隨序列成長的 K/V;爆了就只能拒單或換頁。
對 decoder-only Transformer,每個 token、每一層都要快取 Key/Value,後續 decode 才不必重算整段歷史。粗估:
因此並發 N 個長對話時,KV 往往比「再塞一個 replica」更先撞牆。工程上常見手段:
- PagedAttention / 分頁 KV:像 OS 分頁一樣按 block 配置,減少碎片、支援動態長度
- Prefix caching:共用 system prompt/RAG 前綴的 KV,降重複 Prefill
- 量化 KV(FP8/INT8):用精度換容量,需評估品質回歸
- 限制 max_concurrency × max_context:最樸素也最有效的容量規劃
該看的是最右邊那段:權重與 activation 是開機就固定的,只有 KV 那一段會隨「並發數 × 上下文長度」即時長大——容量規劃要規劃的就是這一段還剩多少。
7B 權重量化後可能輕鬆進 24GB,但 32k context × 高並發會把剩餘 HBM 吃光。容量規劃要以 權重 + 目標並發 × 目標上下文的 KV 一起算,而不是只看 checkpoint 大小。
TTFT vs Throughput:怎麼取捨
互動產品護 TTFT;批次摘要/離線評測護吞吐;同一集群常用隔離與優先權拆開。
| 優化方向 | 常見手法 | TTFT | Throughput | 風險 |
|---|---|---|---|---|
| 加大 continuous batch | 提高 max_num_seqs / 佇列深度 | 變差 | 變好 | 長尾延遲、不公平 |
| Prefill 優先/分離 | 獨立 prefill worker、chunked prefill | 變好 | 可能略降 | 調度複雜、資源切分 |
| 縮短有效 prompt | RAG top-k、摘要、prompt cache | 變好 | 變好 | 品質/召回取捨(屬應用層) |
| 投機解碼 / 小模型草稿 | speculative decoding | 視情況 | 常變好 | 接受率低時白算 |
沒有免費午餐:多數 serving 旋鈕是在「體感延遲」與「單位 GPU 產出」之間滑動。
實務上建議用產品 SLA 反推:聊天/客服把 TTFT p95 當硬約束,再在約束內最大化 tokens/s;離線評測或大量摘要任務則可接受較高 TTFT,換取更高 batch。必要時分池(interactive pool vs batch pool),比在單一佇列上調一個神奇參數更穩。
模型(誠實申報,教學示意非實測):一次 decode iteration 耗時 9 + 0.55 × b ms(固定項是整批共攤的權重讀取),prefill 吞吐 9000 token/s,但 prefill 只能吃 decode 沒用掉的那部分 GPU——batch 越大,prefill 能分到的比例越小。排隊延遲用 M/M/1 近似 ρ/(1−ρ)。KV 每 token 128 KiB(FP16)/64 KiB(FP8)。
看軌跡的最低點:batch 上限往右拉,可服務吞吐一路上升,但 TTFT 先降後升——降是因為排隊變短,升是因為 decode 吃走了 prefill 的預算。最低點右邊那一段,就是「用首 token 換吞吐」真正發生的地方。
互動 Demo:Batching 與 KV 如何改寫延遲/吞吐
編好請求台帳與 GPU/KV 預算,逐 tick 推進排程器,看 slot 怎麼換人、KV 什麼時候觸頂。
每按一次「下一 tick」就真的跑一次 iteration:先處理到達、再決定誰能 admit、再把該 tick 的計算預算分給 prefill 與 decode。規則(誠實申報,教學示意非實測):一個 tick = 20 ms、計算預算 512 unit;prefill 每 token 1 unit;decode 每 iteration 固定 160 unit(權重讀取,整批共攤)+每序列 6 unit。KV 每 token 128 KiB(=2 × 32 layers × 8 kv_heads × 128 head_dim × 2 B,FP16),故 1 GiB = 8192 token。Admission 以「已寫入 + 已承諾」的 KV 計;KV 觸頂而 decode 還要長時,最晚進來的那筆會被搶佔重算。
| 請求 | 類型(可點) | prompt | 生成 | 到達 tick | TTFT | 完成 tick | 進度 |
|---|
該看的是格帶上「同一個 slot 換人」的那幾格:continuous batching 讓完成的請求立刻讓出 slot 給排隊者,而靜態 Batch 會讓整條格帶先空著等齊、再一起走——右側 KV 量規則負責告訴你,這場調度還有沒有記憶體可以調。
面試折衷與實務選型
把 serving 講成可落地的容量、SLA 與觀測問題,而不是背一串框架名稱。
| 面試常問 | 建議答法骨架 |
|---|---|
| 為什麼 LLM API 比一般 REST 難擴? | 自回歸、狀態(KV)隨長度成長、串流長連線、延遲指標要拆 TTFT/TPOT。 |
| 如何提高 GPU 利用率? | Continuous batching、paged KV、prefix cache;必要時 speculative decoding。 |
| 如何保證互動體驗? | TTFT SLO、prefill 優先或分池、限制超長 context、背壓與排隊可見性。 |
| 顯存爆了怎麼辦? | 降並發、縮 max_len、KV 量化、分頁換出、拒絕過長請求並回報可重試。 |
先定義 SLA 與上下文分佈,再選 runtime/並行策略。開源棧常見路徑是「連續批處理 + 分頁 KV」;自建需補齊:佇列背壓、每請求取消、token 級計費、以及 Prefill/Decode 指標儀表板。框架名會變,這幾塊不會。
模型訓練、LoRA 熱更新、多模型路由、以及 Agent/Tool 編排都不在範圍。那些會改變「請求長什麼樣」,但不改變「GPU 上 Prefill → Decode → KV」這條物理約束。