LLM 部署的挑戰:延遲、吞吐、成本
從延遲、吞吐到成本,拆解 LLM 部署的三大核心指標、它們之間的取捨,以及為什麼 LLM 部署比傳統服務難得多。
LLM 部署的挑戰:延遲、吞吐、成本
前面兩個系列,我們打造了 Skill 系統,也用 Harness 驗證了品質。
現在,你的 Agent 已經「能跑」也「可靠」了。
但當你要把它部署到生產環境,面對真實的使用者與流量時,三個新的挑戰會立刻浮現:延遲、吞吐、成本。
這一篇,我們要進入應用三部曲的最後一部:部署與推理優化。
我們會拆解 LLM 部署的三大核心指標、它們之間的取捨、延遲的組成、吞吐與批次的關係,以及成本結構,並用具體數據說明為什麼 LLM 部署比傳統服務難得多。
一、先說一個故事
假設你開發了一個客服 Agent,在測試環境表現優異。
你把它部署到生產環境,期待它服務數千名使用者。
上線第一天,問題就來了:
- 使用者抱怨:「為什麼回答一個問題要等 10 秒?」
- 運維抱怨:「GPU 使用率只有 20%,但請求一直在排隊。」
- 老闆抱怨:「這個月的 API 帳單是上個月的 5 倍。」
你回頭檢查,才發現:
- 延遲:每次請求要等模型逐字生成,首字延遲(TTFT)就花了 2 秒。
- 吞吐:GPU 大部分時間在等記憶體,沒有充分利用。
- 成本:每次請求都要跑完整的模型,沒有快取、沒有批次。
這些問題,就是 LLM 部署的核心挑戰。
傳統服務的部署,關注的是 CPU、記憶體、QPS。
LLM 服務的部署,關注的是延遲、吞吐、成本,以及它們之間的三角取捨。
二、三大核心指標
1. 延遲(Latency)
定義:從使用者送出請求,到收到完整回應的時間。
LLM 的延遲不是單一數字,而是由多個部分組成:
總延遲 = 網路延遲 + 排隊延遲 + 首字延遲(TTFT)+ 生成延遲(TPOT × 輸出長度)
| 組成 | 說明 | 典型值 |
|---|---|---|
| 網路延遲 | 請求往返的時間 | 10-100 ms |
| 排隊延遲 | 等待 GPU 空閒的時間 | 0-數秒 |
| 首字延遲(TTFT) | 從請求到第一個 token 產出 | 100 ms - 2 s |
| 生成延遲(TPOT) | 每個後續 token 的產出時間 | 10-50 ms |
TTFT(Time To First Token) 是使用者感受最強烈的指標。
當你按下送出,等了 2 秒才看到第一個字,體感就是「很慢」。
TPOT(Time Per Output Token) 決定了整體的等待時間。
如果 TPOT 是 30 ms,生成 200 個 token 就需要 6 秒。
2. 吞吐(Throughput)
定義:單位時間內能處理的請求數或 token 數。
常見的衡量方式:
- Requests Per Second(RPS):每秒處理多少請求。
- Tokens Per Second(TPS):每秒生成多少 token。
- Goodput:在滿足延遲 SLO 的前提下,能處理多少請求。
吞吐與延遲是矛盾的。
提高吞吐通常需要批次處理,但批次會增加單一請求的延遲。
3. 成本(Cost)
定義:每處理一個請求或生成一個 token 的花費。
成本的主要來源:
| 來源 | 說明 | 佔比 |
|---|---|---|
| GPU 算力 | 租用或購買 GPU 的費用 | 60-80% |
| 記憶體 | GPU 記憶體、系統記憶體 | 10-20% |
| 網路頻寬 | 傳輸請求與回應 | 5-10% |
| 儲存 | 模型權重、快取 | 5-10% |
成本與延遲、吞吐也是矛盾的。
要降低延遲,需要更快的 GPU;要提高吞吐,需要更大的批次;兩者都會增加成本。
三角取捨
延遲
/ \
/ \
/ \
吞吐 ———————— 成本
你無法同時最佳化三者。
你必須根據應用場景,選擇優先級:
| 應用場景 | 優先級 | 說明 |
|---|---|---|
| 即時對話 | 延遲 > 吞吐 > 成本 | 使用者不能等 |
| 批次處理 | 吞吐 > 成本 > 延遲 | 可以慢慢跑 |
| 大規模服務 | 成本 > 吞吐 > 延遲 | 成本決定可行性 |
三、為什麼 LLM 部署比傳統服務難?
1. 自回歸生成:一個一個 token 產出
傳統服務:輸入 → 計算 → 輸出。一次完成。
LLM:輸入 → 產出 token 1 → 產出 token 2 → … → 產出 token N。
每一步都依賴前一步的結果,無法平行。
傳統服務: [輸入] → [處理] → [輸出] (一次)
LLM: [輸入] → [t1] → [t2] → [t3] → ... (序列)
這意味著:
- 延遲與輸出長度成正比:輸出越長,等待越久。
- GPU 利用率低:每個 token 的計算量很小,GPU 大部分時間在等。
2. 記憶體密集:模型太大,記憶體是瓶頸
一個 70B 參數的模型,用 FP16 儲存需要 140 GB 記憶體。
這超過單張 GPU 的容量,需要多張 GPU 或多種優化技術。
更麻煩的是:推理過程中,GPU 的算力往往不是瓶頸,記憶體頻寬才是。
GPU 算力:100 TFLOPS
記憶體頻寬:2 TB/s
一個 token 的計算需要讀取 140 GB 的權重
→ 記憶體頻寬限制:140 GB / 2 TB/s = 70 ms
→ 算力限制:遠小於 70 ms
結論:記憶體頻寬是瓶頸
這被稱為 Memory Wall(記憶體牆)。
3. KV Cache:記憶體殺手
自回歸生成需要快取之前的 Key 和 Value,避免重複計算。
但 KV Cache 的大小與序列長度和批次大小成正比:
KV Cache 大小 = 2 × 層數 × 頭數 × 頭維度 × 序列長度 × 批次大小 × 精度
對於一個 70B 模型,序列長度 4096,批次大小 32:
2 × 80 × 64 × 128 × 4096 × 32 × 2 bytes ≈ 40 GB
這意味著:
- 批次越大,KV Cache 越大,可能超過 GPU 記憶體。
- 序列越長,KV Cache 越大,限制了最大上下文長度。
4. 動態工作負載:每個請求長度不同
傳統服務:請求大小相對固定。
LLM 服務:每個請求的輸入長度、輸出長度都不同。
請求 A:輸入 100 tokens,輸出 50 tokens
請求 B:輸入 2000 tokens,輸出 500 tokens
請求 C:輸入 50 tokens,輸出 1000 tokens
這讓批次處理變得複雜:
- 短請求被長請求拖累。
- 批次內的請求完成時間不同。
- 需要動態調度。
5. 成本結構不同
傳統服務:成本主要在 CPU 和網路。
LLM 服務:成本主要在 GPU。
一張 A100 GPU 每小時約 3-5 美元。
如果沒有充分利用,成本會非常驚人。
四、延遲的深入拆解
讓我們更具體地拆解延遲。
首字延遲(TTFT)
TTFT 是從請求發出到第一個 token 產出的時間。
組成:
TTFT = 網路延遲 + 排隊延遲 + 預填充時間(Prefill)
預填充(Prefill) 是處理輸入序列的階段。
模型需要讀取所有輸入 token,計算注意力,生成第一個輸出。
預填充的計算量與輸入長度成正比:
Prefill 時間 ≈ 輸入長度 × 每 token 的計算時間
對於一個 2000 token 的輸入,預填充可能需要 500 ms。
生成延遲(TPOT)
TPOT 是每個後續 token 的產出時間。
組成:
TPOT = 解碼時間(Decode)
解碼(Decode) 是生成後續 token 的階段。
每一步只需要計算一個 token,但需要讀取整個 KV Cache。
TPOT 主要受記憶體頻寬限制:
TPOT ≈ 模型大小 / 記憶體頻寬
對於一個 70B 模型(FP16,140 GB),記憶體頻寬 2 TB/s:
TPOT ≈ 140 GB / 2 TB/s = 70 ms
這意味著,即使 GPU 算力再強,TPOT 也無法低於這個下限。
總延遲的估算
總延遲 = TTFT + TPOT × 輸出長度
| 輸出長度 | TTFT | TPOT | 總延遲 |
|---|---|---|---|
| 50 tokens | 200 ms | 30 ms | 1.7 s |
| 200 tokens | 500 ms | 30 ms | 6.5 s |
| 1000 tokens | 2000 ms | 30 ms | 32 s |
可以看到,輸出長度對總延遲的影響是巨大的。
延遲的優化方向
| 優化方向 | 方法 | 效果 |
|---|---|---|
| 降低 TTFT | 減少輸入長度、優化預填充 | 高 |
| 降低 TPOT | 量化、KV Cache 優化 | 中 |
| 減少輸出長度 | 更好的 Prompt、限制 max_tokens | 高 |
| 減少排隊 | 增加 GPU、優先級調度 | 中 |
| 串流輸出 | 邊生成邊回傳 | 體感大幅改善 |
串流輸出(Streaming) 是最有效的體感優化。
即使總延遲不變,使用者看到第一個字就能開始閱讀,體感快很多。
五、吞吐的深入拆解
批次處理:吞吐的關鍵
GPU 在處理單一請求時,利用率往往很低。
批次處理讓多個請求同時計算,大幅提升利用率。
單一請求:GPU 利用率 20%
批次 8: GPU 利用率 60%
批次 32: GPU 利用率 90%
但批次處理有代價:
- 延遲增加:請求需要等待批次湊滿。
- 記憶體增加:KV Cache 與批次大小成正比。
靜態批次 vs 連續批次
靜態批次(Static Batching):等所有請求到齊,一起處理。
時間軸:
[t=0] 等待請求 1-8
[t=1] 處理請求 1-8
[t=5] 全部完成
問題:短請求要等長請求,浪費時間。
連續批次(Continuous Batching):請求完成就移出,新請求隨時加入。
時間軸:
[t=0] 請求 1-4 開始
[t=1] 請求 5 加入,請求 1 完成
[t=2] 請求 6 加入,請求 2 完成
...
這樣 GPU 幾乎不會閒置。
吞吐的計算
吞吐 = 批次大小 / 每個批次的處理時間
對於一個 70B 模型:
| 批次大小 | 每批次時間 | 吞吐(tokens/s) |
|---|---|---|
| 1 | 70 ms | 14 |
| 8 | 100 ms | 80 |
| 32 | 200 ms | 160 |
| 64 | 350 ms | 183 |
可以看到,批次越大,吞吐越高,但邊際效益遞減。
吞吐的優化方向
| 優化方向 | 方法 | 效果 |
|---|---|---|
| 增加批次 | 連續批次、動態批次 | 高 |
| 減少計算 | 量化、稀疏化 | 中 |
| 減少記憶體 | KV Cache 壓縮、PagedAttention | 高 |
| 平行化 | 張量平行、流水線平行 | 高 |
| 加速解碼 | 推測解碼 | 中 |
六、成本的深入拆解
成本結構
LLM 服務的成本可以拆解為:
總成本 = GPU 成本 + 記憶體成本 + 網路成本 + 儲存成本
其中 GPU 成本佔大頭:
GPU 成本 = GPU 數量 × 單價 × 使用時間
成本的計算範例
假設你用一張 A100(每小時 3 美元)部署一個 7B 模型:
| 指標 | 數值 |
|---|---|
| GPU 單價 | $3/小時 |
| 每日運行 | 24 小時 |
| 每日成本 | $72 |
| 每月成本 | $2160 |
| 每秒成本 | $0.00083 |
如果每秒處理 10 個請求:
每請求成本 = $0.00083 / 10 = $0.000083
如果每秒只處理 1 個請求:
每請求成本 = $0.00083 / 1 = $0.00083
吞吐越低,每請求成本越高。
成本優化的方向
| 優化方向 | 方法 | 效果 |
|---|---|---|
| 提高吞吐 | 批次處理、連續批次 | 高 |
| 減少 GPU | 量化、模型壓縮 | 高 |
| 減少運行時間 | 自動擴縮、按需啟動 | 中 |
| 快取 | 相同請求快取結果 | 高 |
| 路由 | 簡單請求用小模型 | 高 |
| 降級 | 高峰時降級服務 | 中 |
快取的威力
如果 30% 的請求是重複的,快取可以節省 30% 的成本。
def cached_inference(prompt: str, cache: dict, model):
"""帶快取的推理"""
cache_key = hash(prompt)
if cache_key in cache:
return cache[cache_key]
result = model.generate(prompt)
cache[cache_key] = result
return result
模型路由
不是所有請求都需要大模型。
def route_request(prompt: str) -> str:
"""根據複雜度路由到不同模型"""
complexity = estimate_complexity(prompt)
if complexity == "simple":
return small_model.generate(prompt)
elif complexity == "medium":
return medium_model.generate(prompt)
else:
return large_model.generate(prompt)
這樣可以大幅降低成本,同時保持品質。
七、一個具體的部署範例
讓我們用一個具體的場景,說明三大指標的取捨。
場景:客服 Agent
需求:
- 每天 10,000 個請求
- 平均輸入 500 tokens
- 平均輸出 200 tokens
- 使用者可接受的延遲:3 秒
選項 A:單一 GPU,無批次
吞吐:1 請求/秒
每日處理:86,400 請求(足夠)
延遲:6.5 秒(超過 3 秒)
成本:$72/天
選項 B:單一 GPU,批次 8
吞吐:8 請求/秒
每日處理:691,200 請求(遠超需求)
延遲:8 秒(超過 3 秒)
成本:$72/天
選項 C:兩張 GPU,批次 4
吞吐:16 請求/秒
延遲:4 秒(接近 3 秒)
成本:$144/天
選項 D:量化 + 批次 4
吞吐:12 請求/秒
延遲:2.5 秒(符合需求)
成本:$72/天(量化後可用較小 GPU)
在這個場景中,選項 D 是最佳解:透過量化降低延遲,同時保持成本。
部署決策清單
1. 確定延遲 SLO(使用者能接受多久?)
2. 估算吞吐需求(每秒多少請求?)
3. 計算成本預算(每月能花多少?)
4. 選擇模型大小(7B / 13B / 70B?)
5. 選擇優化技術(量化、批次、快取)
6. 監控與調優(持續觀察三大指標)
八、監控與調優
部署不是終點,而是起點。
你需要持續監控三大指標,並根據實際情況調優。
關鍵指標
| 指標 | 說明 | 目標 |
|---|---|---|
| TTFT | 首字延遲 | < 500 ms |
| TPOT | 每 token 延遲 | < 30 ms |
| RPS | 每秒請求數 | 滿足需求 |
| GPU 利用率 | GPU 使用率 | > 70% |
| 每請求成本 | 平均成本 | 符合預算 |
| 錯誤率 | 失敗請求比例 | < 1% |
監控工具
class LLMMetrics:
"""LLM 服務指標收集"""
def __init__(self):
self.ttft_records = []
self.tpot_records = []
self.request_count = 0
self.error_count = 0
self.total_tokens = 0
def record_request(
self,
ttft: float,
tpot: float,
output_tokens: int,
success: bool = True,
):
"""記錄一次請求"""
self.request_count += 1
self.total_tokens += output_tokens
if success:
self.ttft_records.append(ttft)
self.tpot_records.append(tpot)
else:
self.error_count += 1
def get_summary(self) -> dict:
"""取得摘要"""
from statistics import mean, median
return {
"total_requests": self.request_count,
"error_rate": self.error_count / max(self.request_count, 1),
"avg_ttft": mean(self.ttft_records) if self.ttft_records else 0,
"p95_ttft": (
sorted(self.ttft_records)[int(len(self.ttft_records) * 0.95)]
if self.ttft_records else 0
),
"avg_tpot": mean(self.tpot_records) if self.tpot_records else 0,
"p95_tpot": (
sorted(self.tpot_records)[int(len(self.tpot_records) * 0.95)]
if self.tpot_records else 0
),
"total_tokens": self.total_tokens,
}
調優策略
| 問題 | 可能原因 | 調優策略 |
|---|---|---|
| TTFT 太高 | 輸入太長、排隊太久 | 減少輸入、增加 GPU、優先級調度 |
| TPOT 太高 | 模型太大、量化不足 | 量化、用小模型、KV Cache 優化 |
| 吞吐太低 | 批次太小、GPU 閒置 | 連續批次、增加批次、平行化 |
| 成本太高 | 吞吐太低、模型太大 | 快取、路由、降級、量化 |
| 錯誤率高 | 超時、記憶體不足 | 增加資源、優化記憶體、重試 |
九、常見的陷阱
1. 只看平均延遲
平均值會掩蓋尾延遲。
# 不好的例子:只看平均值
avg_latency = 1.5 # 看起來很好
# 好的例子:看 P95、P99
p95_latency = 8.0 # 5% 的請求要等 8 秒
p99_latency = 15.0 # 1% 的請求要等 15 秒
2. 忽略 TTFT
TTFT 是使用者感受最強烈的指標。
# 不好的例子:只優化總延遲
# 好的例子:同時優化 TTFT 和 TPOT
3. 沒有串流輸出
串流輸出是最有效的體感優化。
# 不好的例子:等全部生成完才回傳
response = model.generate(prompt)
return response
# 好的例子:邊生成邊回傳
for token in model.stream(prompt):
yield token
4. 批次太大
批次太大會增加延遲,且邊際效益遞減。
# 不好的例子:批次 128
# 好的例子:批次 8-32,根據實際情況調整
5. 沒有監控
沒有監控,你不知道系統的實際表現。
# 不好的例子:部署後就不管了
# 好的例子:持續監控 TTFT、TPOT、吞吐、成本
6. 忽略成本
只關注效能,忽略成本。
# 不好的例子:用最貴的 GPU,只為降低延遲
# 好的例子:在延遲與成本之間取得平衡
7. 沒有快取
重複的請求重複計算。
# 不好的例子:每次都跑完整模型
# 好的例子:快取相同請求的結果
8. 沒有降級機制
高峰時服務崩潰。
# 不好的例子:所有請求都用大模型
# 好的例子:高峰時降級到小模型,或排隊
十、總結:三角取捨的藝術
讓我們回顧這一篇的核心:
- 三大核心指標:延遲、吞吐、成本,三者互相矛盾。
- 為什麼 LLM 部署難:自回歸生成、記憶體密集、KV Cache、動態負載、成本結構。
- 延遲的拆解:TTFT、TPOT、總延遲的估算。
- 吞吐的拆解:批次處理、連續批次、吞吐的計算。
- 成本的拆解:GPU 成本、快取的威力、模型路由。
- 具體範例:客服 Agent 的四個選項與最佳解。
- 監控與調優:關鍵指標、監控工具、調優策略。
- 常見陷阱:只看平均、忽略 TTFT、沒有串流、批次太大、沒有監控、忽略成本、沒有快取、沒有降級。
LLM 部署不是單純的「把模型跑起來」,而是在延遲、吞吐、成本之間找到平衡。
沒有通用的最佳解,只有根據場景的取捨。
理解了三大指標與它們的取捨,你就有了部署 LLM 系統的基礎。
接下來,我們要深入每個優化技術,從最關鍵的開始:KV Cache。
下一篇預告
《KV Cache:為什麼推理需要它》
我們會解釋 KV Cache 的原理、它為什麼是推理的必要機制、它的大小如何計算、它如何成為記憶體瓶頸,以及有哪些優化技術(MQA、GQA、PagedAttention)可以緩解這個問題。