跳至主要内容

Day 26:LLMOps — vLLM、KV cache 與成本工程

· 閱讀時間約 7 分鐘
Willis Chen
Tech Instructor

今天是雙主軸真正交會的一天。

  • 主軸一的讀者:這是 Day 13(延遲與吞吐)在 LLM 上的延伸,你的直覺全部適用,只是數字大一個量級。
  • 主軸二的讀者:這是你的系統能不能規模化的關鍵。做得出 demo 與撐得住一萬人,差別就在今天。

Day 02 提到 LLMOps 全台只有 19 筆職缺——但那是因為這個詞還沒進入招募用語。實際上做這件事的人,需求很大。

今天要解決的問題​

三個階段性的問題,多數團隊會依序遇到:

第一階段(PoC):用 API,很順利,每月幾百塊。 第二階段(上線):使用者變多,帳單變成每月六位數。主管開始問「能不能便宜一點」。 第三階段(規模化):算了一下,自建 GPU 好像划算——但真的划算嗎? 而且法務說某些資料不能出境。

今天回答這三個問題:怎麼省、什麼時候該自建、自建要注意什麼。

📊 職缺訊號

「生成式 AI 部署與 LLMOps」出現在 38.7% 的生成式 AI 職缺中,同時「模型部署與推論服務」出現在 49.2% 的 MLOps 職缺。這兩個數字重疊的部分,就是今天的內容——也是目前市場上最稀缺的組合。


一、現象:LLM 服務的成本結構​

LLM 成本儀表板

圖 26-1:LLM 成本的歸因、優化成效與單次請求拆解(示範性推估)。左圖顯示導入模型路由與快取後的成本下降;右圖是單次請求的成本組成。

先看右圖(Day 07 談過,今天要動手處理):最貴的是「固定前綴」與「輸出」。所以省錢的順序很明確:

順序手段典型效果難度
1限制輸出長度(max_tokens + 提示要求精簡)20–40%極低
2prompt caching(固定前綴前置且逐字穩定)輸入端約 50–60%低
3模型路由(簡單任務用小模型)30–50%中
4語意快取(高重複性場景)10–30%中(有誤命中風險)
5精簡檢索脈絡(提高檢索精準度後降 top-k)10–20%中
6自建推論規模夠大時 50%+高

注意「自建」排在最後。 前五項都是幾天內能做完的軟性優化,自建則是一個需要人力持續維護的決定。

二、原理:為什麼 vLLM 能快好幾倍​

主軸二的讀者可能沒想過:同一張 GPU、同一個模型,換一個推論引擎,吞吐可以差三到四倍。原因在於 KV cache 的管理方式。

KV cache 記憶體與批次策略

圖 26-2:KV cache 記憶體(依公式精算)與批次策略的效益(示範性推估)。左圖:批次 × 長度的乘積效應;右圖:連續批次與 PagedAttention 對吞吐與顯存碎片的影響。

左圖的計算(Day 07 提過,這裡是完整版):

KV cache = 2(K 與 V)× 層數 × KV head 數 × head_dim × 位元組數 × 序列長度 × 批次

8B 模型(32 層、8 KV head、head_dim 128、fp16):
每 token 每序列 = 2 × 32 × 8 × 128 × 2 B = 131,072 B = 128 KiB

批次 16、序列 8,192:128 KiB × 8192 × 16 ≈ 17.2 GB
加上權重 16 GB → 約 33 GB,一張 80GB 卡放得下但要留餘裕

右圖的三個改進:

  • 靜態批次:一批請求要全部跑完才換下一批。最短的請求要等最長的,GPU 大量閒置。
  • 連續批次(continuous batching):某個序列生成完就立刻離開,新請求馬上補進來。吞吐約 3.6 倍。
  • PagedAttention:借用作業系統分頁的概念管理 KV cache,把顯存碎片從 58% 降到 4%——同樣的顯存能塞更多併發。

💡 給主軸二讀者的白話版

你不需要看懂 PagedAttention 的實作。你需要知道的是:如果你現在用 Hugging Face transformers 的 generate() 直接對外服務,換成 vLLM 通常能立刻獲得數倍吞吐——不用改模型、不用改應用程式碼(因為它提供 OpenAI 相容 API)。

三、動手:自建服務與成本試算​

3.1 起一個 OpenAI 相容的服務​

uv add vllm

python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen2.5-7B-Instruct \
--gpu-memory-utilization 0.90 \ # 保留 10% 給碎片與突發,別設 1.0
--max-model-len 8192 \ # 直接決定 KV cache 上限,別設超過實際需求
--max-num-seqs 64 \ # 最大併發序列數
--port 8000

應用端幾乎不用改——因為它是 OpenAI 相容介面:

from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")
# 之後的呼叫方式與雲端 API 完全相同

三個參數的實務意義:

參數設太大設太小
--gpu-memory-utilizationOOM 崩潰併發數上不去
--max-model-lenKV cache 吃光顯存,併發數暴跌長文件請求被拒絕
--max-num-seqs排隊變長、TTFT 惡化吞吐上不去

3.2 壓測:LLM 服務要看的三個指標​

"""bench.py —— LLM 服務的壓測,指標與傳統 API 不同。"""
import asyncio, time

async def one_request(client, prompt):
t0 = time.perf_counter()
ttft = None
tokens = 0
stream = await client.chat.completions.create(
model=MODEL, messages=[{"role": "user", "content": prompt}],
stream=True, max_tokens=300)
async for chunk in stream:
if chunk.choices[0].delta.content:
if ttft is None:
ttft = time.perf_counter() - t0 # 首個 token 抵達時間
tokens += 1
total = time.perf_counter() - t0
return {"ttft": ttft, "total": total,
"tpot": (total - ttft) / max(tokens - 1, 1), # 每個後續 token 的時間
"tokens": tokens}

# 三個必看指標:
# TTFT → 使用者的「等待感」(受 prefill 與排隊影響)
# TPOT → 生成的「流暢感」(受 decode 影響)
# 吞吐 → 你的容量(總輸出 token / 秒)

這三個指標要分開訂 SLO。 常見的目標:TTFT P95 < 1 秒(等待感)、TPOT < 50 ms(約每秒 20 字,比多數人閱讀速度快)。只看端到端延遲會看不出問題出在 prefill 還是 decode。

3.3 自建 vs API 的損益平衡​

"""llm_cost.py —— 把假設攤開來算(Day 15 的方法,換成 LLM 情境)。"""
# API 方案
API_INPUT, API_OUTPUT = 1.0, 5.0 # 每百萬 token 的相對單價
daily_in, daily_out = 400_000_000, 20_000_000 # 每日 token 量

api_monthly = (daily_in / 1e6 * API_INPUT + daily_out / 1e6 * API_OUTPUT) * 30

# 自建方案(Day 15 的成本模型)
GPU_HOURLY = 3.0 # 雲端 GPU 每小時(或地端攤提)
GPUS_NEEDED = 3 # 由容量規劃決定:尖峰吞吐 ÷ 單卡吞吐 + 冗餘
OPS_MONTHLY = 400 # 維運人力分攤——自建最容易被低估的成本
selfhost_monthly = GPU_HOURLY * 24 * 30 * GPUS_NEEDED + OPS_MONTHLY

print(f"API 方案:{api_monthly:>10.0f} / 月")
print(f"自建方案:{selfhost_monthly:>10.0f} / 月")
print("建議:", "自建" if selfhost_monthly < api_monthly * 0.7 else "續用 API")
# 門檻設 0.7 而非 1.0:自建要明顯划算才值得承擔維運複雜度

📌 那個 0.7 是刻意的

自建除了帳面成本,還有隱形成本:模型升級要自己做、出事要自己修、半夜掛了要有人接。只有在明顯划算(省 30% 以上)時才值得——除非你的決策理由是資料主權,那就不是成本問題。

四、取捨:模型路由與發布管理​

模型路由是最被低估的成本手段:不是所有請求都需要最強的模型。

"""router.py —— 依任務複雜度分流,簡單任務用便宜模型。"""
async def route_and_answer(question, context):
# 用小模型先判斷複雜度(這次判斷本身很便宜)
complexity = await classify_complexity(question) # simple / standard / complex

model = {"simple": SMALL_MODEL, # 分類、擷取、格式轉換
"standard": MID_MODEL, # 一般問答、摘要
"complex": LARGE_MODEL}[complexity] # 多步推理、長文件分析

try:
return await call_llm(model=model, ...)
except (RateLimitError, APIError):
return await call_llm(model=FALLBACK_MODEL, ...) # 跨供應商冗餘

LLM 服務的發布管理——這是 Day 11 品質門檻的 LLMOps 版本。要注意的是,LLM 服務有四種資產會變動:

資產誰會改發布前要做
模型(版本或供應商)工程師/供應商自動升版跑全套評測 + 人工抽查
提示工程師、有時是 PM跑全套評測(Day 24)
檢索索引文件更新自動觸發跑檢索端指標
工具定義與權限工程師跑注入測試(Day 25)

4.1 一個常被問到的問題:小模型真的夠用嗎?​

「模型路由」聽起來很好,但實務上第一個疑問通常是:小模型的品質夠嗎?

答案取決於任務類型。經驗上的分界線是:任務越接近「格式轉換」,小模型越夠用;越接近「開放式推理」,差距越大。

任務小模型是否夠用說明
意圖分類、情緒判斷✅ 通常夠選項有限,且可用 few-shot 補強
結構化欄位擷取✅ 通常夠有明確的 schema 約束
查詢改寫(Day 19)✅ 夠高頻但簡單,用大模型是浪費
摘要⚠️ 視長度短文可以,長文的重點取捨會明顯變差
多步推理、複雜問答❌ 差距明顯這才是大模型的價值所在
程式碼生成❌ 差距明顯錯一個字就不能跑,容錯率低

判斷方法還是回到評測(Day 24):把你的評測集用小模型跑一次,看掉多少分。如果只掉 2%、成本降 90%,那顯然值得;如果掉 15%,那就別省這個錢。這個決定不該靠感覺,一小時就能量出來。

⚠️ 一個 LLMOps 特有的風險:供應商自動升版

你沒有改任何東西,但供應商把 model-latest 指向了新版本,你的產品行為就變了。正式環境一定要釘住明確的模型版本號,並在自己決定的時間點升級——升級時走完整的評測與金絲雀流程。

這是傳統 MLOps 沒有的問題:你的相依項會自己動。


今日小結​

  • 省錢的順序:限制輸出長度 → prompt caching → 模型路由 → 語意快取 → 精簡檢索脈絡 → 最後才考慮自建。前五項幾天內能做完。
  • KV cache 每 token 每序列 128 KiB(8B 級模型),批次 × 長度的乘積會迅速吃光顯存——這是併發上不去的真正原因。
  • vLLM 的優勢來自 continuous batching(約 3.6 倍吞吐)與 PagedAttention(顯存碎片 58% → 4%);提供 OpenAI 相容 API,應用端幾乎不用改。
  • 三個參數要一起調:gpu-memory-utilization、max-model-len、max-num-seqs。
  • LLM 服務要分開看 TTFT(等待感) 與 TPOT(流暢感),各訂 SLO;只看端到端延遲查不出問題在哪。
  • 自建要明顯划算(省 30% 以上)才值得,因為有維運隱形成本——除非決策理由是資料主權。
  • LLM 服務有四種會變動的資產(模型、提示、索引、工具),每一種都要有對應的發布檢核。
  • 正式環境要釘住模型版本號——你的相依項會自己動。

明天預告​

技術面完成,明天處理制度面:治理。AI 風險矩陣怎麼畫、歐盟 AI Act 的時程對台灣團隊有什麼影響、模型檔案的供應鏈風險(那個很多人不知道的 pickle 問題),以及一套能真正落地的權限與稽核設計。

延伸閱讀​

  • 📄 Kwon et al., Efficient Memory Management for Large Language Model Serving with PagedAttention (2023)——vLLM 的原始論文
  • 📄 vLLM 官方文件——參數調校與部署的第一手資料

← Day 25 | Day 27 →

本文同步發布於 iThome 鐵人賽;Blog 版本為站內閱讀與系列導覽的主要版本。