工程實作 13 min read

壓測 LLM 推論服務,別只問「能撐幾個 client」——用 goodput 回答容量

「這個 AI 推論服務能不能上線」不能用「最多幾個 client」回答:連線數只描述掛得住幾條連線,不告訴你「在使用者能接受的延遲下,每秒服務得動幾個成功請求」。這篇把容量改寫成一句可驗收的話——在指定 workload 與流量模型下,仍同時滿足成功率、TTFT/TPOT/E2E、且零 OOM/restart 的最大 offered load,以及對應的 SLO-qualified goodput——並做成一個能跑的 skill。內容含:goodput vs completed throughput 差在哪、串流式 LLM 要量哪四個指標、為什麼 HTTP 2xx 不等於合格、coordinated omission 怎麼藏掉壅塞、穩定性缺 probe 為何標 UNKNOWN 不標 PASS。附一次真實 smoke 的樣本:reasoning 模型 TTFT 從 0.9 秒抖到 85 秒、max_tokens 被推理和答案一起吃掉。

壓測 LLM 推論服務,別只問「能撐幾個 client」——用 goodput 回答容量
本文目錄 · 7

要回答「這個 AI 推論服務能不能上線」,我需要一個比「最多幾個 client」更硬的答案。連線數只描述「同時能掛幾條連線」,不告訴你「在使用者能接受的延遲下,每秒真的服務得動幾個成功請求」。這兩件事在一個會排隊、會被 KV cache 撐爆、會冷啟動的 LLM 服務上,差距可以很大。

所以我把容量問題改寫成一句可驗收的話,並做成一個能跑的 skill。這篇講那句話怎麼定義、一個 LLM 服務至少要看哪五個維度,以及我拿一顆 26B 模型從 c1 掃到 c64 的實測成績,長什麼樣。

為什麼「最多幾個 client」不是容量答案

容量的正式定義,我用這一句:

在指定 workload 與流量模型下,仍同時滿足成功率、TTFT/TPOT/E2E(或語音的 RTF)、且沒有 OOM/restart 的最大 offered load,以及對應的 SLO-qualified goodput。

拆開看兩個關鍵詞。

offered load 是你「送進去」的速率,不是服務「接得住」的速率。壓測工具送 20 req/s,服務只消化得動 12 req/s,那多出來的 8 會排隊、延遲爆掉。如果你只盯著「client 數」,會誤以為連線開得越多容量越大,實際上超過某個點之後,多開的連線只是讓每個請求都變慢。

goodput 是「成功且全部延遲條件都達標」的請求速率。這跟 completed throughput(單純完成的請求速率)是兩個數字:一個請求可以「回 200、但慢到使用者早就關掉分頁」,它算 completed,不算 good。容量報告要報的是 goodput,不是 completed。

所以「能撐幾個 client」只回答了連線上限這一小塊。正式容量,要用符合全部 SLO 的 goodput 來回答。

壓測 LLM 要量哪些指標

一般 HTTP 壓測量「平均延遲 + QPS」就夠了。串流式的 LLM 不行,因為使用者體感被拆成兩段:第一個字多久出來,以及後面的字吐多快。我固定量這四個:

指標定義為什麼重要
TTFT從原訂到達時間,到第一個非空 token/chunk使用者盯著空白畫面等多久
TPOT第一個 token 之後,平均每個 output token 的時間生成過程順不順、會不會一頓一頓
E2E整個請求從到達到最後一個 token整體體感
token throughputinput/output token 每秒吞吐跨請求聚合的真實產能
有一個容易踩的替代陷阱:沒有 tokenizer 或 usage 資料時,會想用「字元數」當 output 量的近似。字元數只能當同一模型內、無 usage 時的 fallback,不能拿來跨模型比較 —— 不同 tokenizer 一個 token 對應的字元數天差地遠,用字元數比兩個模型等於在比兩把不同刻度的尺。OpenAI SSE 這類介面優先吃 response 的 usage;服務不回 usage 時,對應的 token SLO 就標「證據不足」,不硬湊。

還有一個串流專屬的指標 inter_chunk_latency(chunk 間抖動)。注意一個 chunk 不一定等於一個 token,所以它跟 TPOT 是兩件事,別混用。

一個 LLM 服務要看哪幾個維度

別只盯「第一個字多快」。一個模型服務至少五個維度一起看,缺一個都會誤判:首字很快,但逐字卡頓一樣難用;延遲全過,但掉 5% 請求就是不能上線。

下面這張表把五個維度講成白話,最後一欄放我這次拿 gamma-26b(一顆 26B 模型、vLLM 服務、OpenAI 相容 SSE)跑 concurrency sweep(c1 → c64)的實測成績。SLO 契約設 P99 TTFT < 3s、P95 E2E < 30s、成功率 100%。

維度指標白話為什麼在乎gamma-26b 實測
首字快不快TTFT送出去到吐第一個字多久體感卡不卡——聊天/串流最敏感,用戶盯著游標轉圈c64 起 P99 跳 3.3s(唯一先破線)
吐字順不順TPOT第一個字之後,每個字之間隔多久讀起來順不順——首字再快,逐字卡頓一樣難用全程 ≤ 0.075s,沒破門檻
整句多久吐完E2E latency從送出到最後一個字一個完整回答要等多久——批處理/API 在乎這個,不在乎首字c1 5.8s → c64 20.0s,沒破 30s
單位時間服務多少throughput/goodput每秒吐幾個 token、每秒完成幾個達標請求同時能扛幾個人——運維排容量、算成本靠這個token/s 51 → 814;goodput 0.25 → 3.99
穩不穩成功率/SLO 達成率會不會掉請求、會不會超時能不能上線——前四個再漂亮,掉 5% 請求就是不合格0 fail;SLO c48 前 100%、c64 98.7%
讀這張表的正確順序,是由後往前。先看最後兩個維度:這次 0 fail、SLO 到 c48 都還 100%,代表服務本身沒垮,數字可以信。再回頭看前三個延遲維度找第一個破線的地方——這次是 TTFT,並發衝到 64 時 P99 從穩定的 0.1s 級跳到 3.3s,先頂到 3s 門檻;而 E2E 到 c64 也才 20s、離 30s 還有餘裕,TPOT 全程穩定。

所以這顆服務的容量瓶頸不是「吐得慢」,是「並發一高、第一個字要等」。這結論只有五個維度一起攤開才看得出來:單看 throughput 一路漲到 814 token/s 會以為還很閒,單看成功率 98.7% 會以為只是零星掉件,要把 TTFT P99 這條線疊上去,才知道「c64 是這份 SLO 下的容量邊界,卡在首字延遲」。這就是為什麼容量報告不能只報一個大數字。

HTTP 2xx 為什麼不等於合格

這是最容易讓報告變好看、卻站不住腳的地方:HTTP 2xx 當成「這個請求 OK」

2xx 只代表服務沒在最外層掛掉。一個回 200、但 E2E 20 秒的請求,對一個 SLO 訂 P95 E2E < 5s 的服務來說是失敗的。goodput 只計「同時符合所有 request-level SLO」的請求,一條都不能少算。這條規則的作用,就是擋住「成功率 99%、但一半請求慢到沒人要用」這種漂亮的假數據。

延遲統計本身也有一個經典陷阱,叫 coordinated omission。閉環壓測(closed-loop)送完一個請求才送下一個,服務一慢,壓測自己就跟著慢下來、少送了請求,於是最該被記錄的那些「塞車時段的高延遲」反而沒被送進去測量,分位數被系統性低估。

我的做法是:要驗真實 server capacity,用開環(open-loop)+ poisson 到達,而且延遲從「原訂到達時間」起算,把壓測工具自己的排隊 delay 也算進去。這樣壅塞藏不住。可重現的 regression 才用 constant + 固定 seed;找最大吞吐才用 closed-loop 掃 concurrency。

穩定性要什麼證據

「零 OOM、零 restart」是很多容量驗收的硬條件。但這條特別容易被輕輕放過 —— 跑完沒看到爆炸,就寫「穩定性 PASS」。

沒有計數器就不算證據。要主張零 restart,得有 before/after 的累積計數:壓測前記一次 restart_count / oom_count / gpu_xid_count,壓測後再記一次,取 delta。health 用 exit code 判。沒有這組 probe,對應項標 UNKNOWN / 證據不足,不能標 PASS。

還有一個看似無害的動作也要擋:缺資料時把數字補成 0。「這台沒接 GPU counter」跟「這台 GPU 錯誤數是 0」是兩件完全不同的事,前者是不知道,後者是知道且為零。缺探針就留白,不補零 —— 資源指標(GPU/CPU/RAM/KV cache/queue)也適用同一條誠實規則。

這套怎麼實際跑起來

流程是一條線:定義 workload 與 SLO 契約 → 選流量模型 → dry-run 煙霧測試 → 逐級加 offered rate 找失敗邊界 → 產出單檔 HTML 報告做 round-trip 驗收。dry-run 會先遮蔽認證 header,確認 payload、response parser、SSE/WS message type、stability probe 都取得到資料,再開始加負載。

煙霧測試長這樣(endpoint 換成你自己的):

python3 scripts/driver_http.py \
  --url "http://your-server:8000/v1/chat/completions" \
  --cases cases.json \
  --payload-template '{"model":"$model","messages":[{"role":"user","content":"$prompt"}],"stream":true}' \
  --adapter openai_sse \
  --clients 1 --rounds 1 \
  --output smoke.json

換一顆 reasoning 模型,量測方法要跟著調

上面 gamma-26b 是標準 completion,指標很乾淨。但同一套 driver 我後來拿去測另一顆會先做內部推理的 reasoning 模型,馬上踩到兩個坑,剛好說明為什麼容量測試不能套一個固定模板了事。

smoke 測試輸出:success_rate 100% 但 slo_attainment 0%、goodput 0;TTFT P50 0.9s/P95 85.4s 的抖動;兩個樣本 output_tokens 都卡在 256;stability probe 缺 counter 標 UNKNOWN

上面這張是那顆 reasoning 模型的一次 smoke 摘要,有兩個現象值得記:

第一,reasoning 模型的 TTFT 會抖得很兇。 同一次跑、兩個 prompt,一個 TTFT 0.9 秒,另一個 85 秒。差距不是網路,是 reasoning 模型在吐正式答案前,會先用一段內部推理(SSE 的 delta.reasoning 欄位)想過一輪,想多久這一輪就佔多久,而 TTFT 量的是「第一個非空輸出」。如果你的 driver 只認 delta.content、不認 reasoning 欄位,會把整段推理期間誤判成「零輸出」,TTFT 直接量歪。

第二,max_tokens 是 reasoning 跟答案一起分的預算。 那次兩個請求的 output 都剛好卡在 256 token —— 因為 256 這個上限被「推理過程 + 最終答案」一起吃掉了。對 reasoning 模型設 output 上限,要把推理的 token 開銷算進去,不然答案會在推理燒完額度後被硬切斷。

同一套壓測工具、換一顆模型,前面 gamma-26b 那張乾淨的 sweep 表就變成這副抖動樣。這不是模型的缺點,是量測方法必須跟著模型型態調的證據。

抓輸出樣本這件事,是刻意加的

壓測工具的本分是回答「多快、多穩」,天生不管「答得好不好」—— 那是語意評測的地盤。但有一個現實問題:要比「同一題,A 模型跟 B 模型各答什麼」,得趁兩顆模型都還在線的那個窗口一次抓齊,模型一卸載,同題對照就永遠做不成了。

所以我讓 driver 在跑效能壓測的同一次,順手保留前 N 個成功請求的完整輸出文本,對齊成並排對照表。邊界劃得很清楚:skill 只負責「趁窗口把證據抓下來」,不負責評分。 誰答得好由語意評測工具判,壓測工具不越界當裁判。這條邊界是刻意的,混在一起會讓兩邊都做不乾淨。

限制與邊界

這套工具有幾條我自己會先講清楚的界線:

  • 跨硬體推估只能當粗估。 可以用記憶體頻寬比對 decode-heavy workload 做參考,但量化格式、KV cache、batching、context length、tensor parallel、互連全都會改變結果。要當採購驗收,目標硬體得用同一份 workload 實測,推估結論一律標「推估非實測」。
  • 它不評正確率、不評幻覺、不評語意品質。 這些是 regression / eval 的範圍,不要指望壓測工具給你「哪個模型答得對」。
  • 極高 QPS 時,先確認壓測端自己沒先飽和。 Python stdlib 的 driver 適合內部容量驗證,真要打到很高的 offered rate,先看 load generator 的 CPU、網路跟 queue delay,必要時換分散式壓測端。
容量測試的價值不在跑出一個大數字,在於那個數字底下每一條 SLO 都有證據撐著,而且缺證據的地方老實標成 UNKNOWN。這件事做對了,「能不能上線」才有辦法用一句話回答。
author
陳彥彤

AI 工程師 · AI 顧問。Java 後端 8 年、AI 工程師 2 年。AI 內訓 · AI 導入顧問 · 前後端與雲端培訓。

support

覺得文章有用可以到 GitHub 給個 star,或是透過信箱聊聊 AI 內訓、AI 導入顧問或前後端 / 雲端培訓。

related

相關文章

[工程實作] · 11min
12-Factor Agents vs 我的遊戲專案:3 條原則,同一個坑
HumanLayer 的 12-Factor Agents 有 24.2k star,我對照自己寫 Splendor 網頁版時踩出的工程紀律,發現其中 3 條原則我半年前就已經做對,這是實測後的對照筆記。
[工程實作] · 19min
我怎麼把別人的 skill 吸進自己產線,做成更強的 skill:一次語意標註實作覆盤
看到好用的 skill,大部分人裝來用;如果你有自己的產線,更值錢的做法是拆它的零件焊進你的流程。我原本有一套截圖審查+手冊產線(/screenshot-review),但手冊圖是「乾圖」沒有圖上標註——這正是 app-screenshots 的 annotate.js 補得上的洞。本文覆盤:我怎麼把它拆進產線,過程撞到一個架構級的坑(標註截不進圖=OVERLAY GONE:agent-browser 每個 CLI 指令是獨立 CDP 呼叫,eval 注入的 overlay 下一指令就蒸發),查根因、用 batch 把 open→eval→screenshot 綁進同一 session 解掉、封裝成 annotate-shot.sh、寫回自己的 skill。全程真的跑真的驗,附 batch 解法的真實成果圖與踩坑總表。
[工程實作] · 16min
軟體有多大?Nesma 功能點分析:只數功能不數行數
Nesma 是荷蘭軟體度量協會(Netherlands Software Metrics Association),主推功能點分析(FPA):不看程式碼行數、只數軟體對使用者做了幾件事,來量一套系統多大。這篇講清楚它是什麼、資料功能(ILF/EIF)+交易功能(EI/EO/EQ)在數什麼、三種計數層級(detailed/estimated/indicative 荷蘭法),以及跟 IFPUG、COSMIC、敏捷 Story Point 差在哪——功能點波動不到 5%、Story Point 團隊間差異可達 400%。對一個 2026 年同時教課又接案的開發者,功能點最實用的價值不是精算,是當一把「客觀的尺」。