大多數人的第一版壓測是一個 Python for 迴圈:串行打 100 發請求、耗時加總除以 100、宣布「平均延遲 800ms」。我自己也寫過。後來真的有十個人同時來問,TTFT 直接衝破十秒——那個 for 迴圈從頭到尾沒有回答過「幾個人同時用會怎樣」。
先講結論:**壓測的重點不是發幾發請求,而是「怎麼發」。**同樣 100 發,串行發、定速發、固定併發發,量到的是三種不同的東西;選錯發法,數字再漂亮都是自欺。今天把發法一次講清楚,後面每一個實測數字都站在它上面。
先破除名字造成的誤會:vllm bench serve 雖然掛著 vLLM 的名字,本質卻是一個 load generator(負載發射器)——掛上 --endpoint-type openai-chat --base-url 之後,任何 OpenAI 相容的 endpoint 它都能壓:llama-server、Ollama、mlx_lm.server,甚至雲端 API(來源:vLLM bench CLI 文件)。
所以本系列的角色分工如圖 1:Mac 只當 server,跑 llama-server / Ollama / mlx_lm.server。理由不是「vLLM 上不了 Mac」——官方 installation 文件已把 Apple Silicon 列在 GPU 平台底下,走 vllm-metal(官方組織下的 plugin,底層 MLX)就裝得起來。
但湊得齊不等於對等:權重要 MLX-native 量化、Metal 沒有容器 GPU passthrough 只能跑 host,Docker 官方的同機對照:同一顆 Llama 3.2 1B 4-bit,llama.cpp 快 vLLM-Metal 約 1.2–1.3×,後者波動還大得多(134–343 t/s)
而發射器永遠跑在受測者對面那台:壓 Mac 時 client 在雙卡機;反過來壓雙卡機時 client 搬回 Mac。
但有個前提:macOS 沒有 vLLM 的預編譯 wheel,pip install vllm 在 Mac 上裝不起來【官方逐字:「Currently, there are no pre-built Apple silicon CPU wheels.」】。想留在同一套指令,就裝上面那個 vllm-metal 外掛
不想動環境,Mac 端改用 GuideLLM(pip install guidellm[recommended],官方支援 macOS,一樣打任何 OpenAI 相容端點、一樣報 TTFT/ITL/分位數)【官方,GuideLLM README】。發射器換人,方法論不換。
同機自打也很實際:發射器要吃 CPU 開幾百條 async 連線、算統計,會跟推論引擎的 tokenizer 與 HTTP handler 搶核心,你量到的是兩個程式打架。

圖 1:發射器永遠住在受測者對面——同機自打量到的是兩個程式搶 CPU,不是 server 的實力。
vllm bench serve 三罪全解:async 併發發射、正式計時前可先熱身、--percentile-metrics ttft,tpot,itl,e2el 搭配 --metric-percentiles 直接吐分位數。
發法由三個參數決定,對應兩種模式(圖 2):
開環(open-loop):--request-rate N——由時鐘決定何時發,每秒 N 發,不管前一發回來了沒。這模擬真實世界:使用者不會因為你的 server 慢就少來。server 追不上時隊伍越排越長,排隊時間灌進 TTFT——這正是你要看的「超載長什麼樣」。
注意陷阱:這個參數預設值是 inf,意思是把全部請求在第 0 秒一次全發(來源:vLLM bench CLI 文件)。不帶參數跑一次就以為量到吞吐,其實 TTFT 全是排隊時間,什麼服務水準都推不出來——這是最常見的誤用。搭配 --burstiness 可以調到達分布:1.0 是 Poisson 到達,小於 1 更陣發、大於 1 更均勻
閉環(closed-loop):--request-rate inf --max-concurrency M——常駐 M 條併發,收到回覆才補下一發。server 變慢,發射自動跟著慢,所以永遠看不到排隊爆炸;它量的是「穩定壓力下的極限吞吐」。

圖 2:開環量使用者的真實體感(含排隊),閉環量 server 的極限吞吐——兩個都要跑,但別把閉環數字當成使用者體感。
兩種都有用,危險的是拿錯:capacity planning 要開環掃出「到達率多少開始崩」;比較引擎極限用閉環
壓完會得到一堆分位數,下一個問題是「多少才算合格」。誠實說:「業界標準 SLO」沒有一手出處,我能找到的只有三個公開錨點,而且性質一個比一個弱:
| 錨點 | 數字 | 性質 |
|---|---|---|
| Anyscale 文件 | TTFT < 500ms、TPOT < 15ms、E2E < 2s | 【官方(示例)】官方自己標明是示例,不是標準(來源:Anyscale benchmarking 文件) |
| Databricks blog | TPOT 100ms = 每人 10 tok/s ≈ 450 words/min,快過一般人閱讀 | 【官方 blog】「夠用」的論證,不是標準(來源:Databricks LLM inference 最佳實務) |
| Spheron blog | 互動式聊天 TTFT p99 300ms/ITL p99 50ms;RAG 聊天 400ms/80ms | 【引用】昨天引過的那份 GPU 雲廠商 blog,作者自陳目標「來自使用者感知,不是技術可達性」——當起點可以,當標準不行 |
所以正確姿勢不是抄別人的 SLO,而是用 --goodput KEY:VALUE 把自家草案寫進壓測:例如 --goodput ttft:2000 tpot:100,意思是「TTFT 2 秒內且 TPOT 100ms 內的請求才算數」,報表直接告訴你達標比率。吞吐的定義從「每秒吐幾個 token」升級成「每秒服務幾個達標請求」——這才是能對老闆交代的數字。
讀圖法也補一課:SemiAnalysis 的 InferenceX(前身 InferenceMAX)每晚重跑各引擎 × 各硬體,畫的是「總吞吐 × 每人互動速度(tok/s/user)」的 Pareto 前緣【引用】(來源:SemiAnalysis InferenceX)。
讀法只有一句:右上角不存在,你永遠在「多服務幾個人」和「每個人快一點」之間選一個點。
我們的 request-rate 掃描,就是在畫自己機器的這條線。
雙卡機單卡只有 12GB,壓測主角我選 8B 的 AWQ(4-bit)版,FP8 當對照。這是算出來的:gpu_memory_utilization 的預設值會隨版本漂移(自己跑 --help 對一次),本篇一律顯式指定 0.9,只拿 12GB 的九成 ≈ 10.8GB。
這 10.8GB 要養三張嘴:
結帳:AWQ 版剩 10.8 − 5.7 − 0.6 ≈ 4.5GB ≈ 36K token 的 KV,8K context 下約 4 條滿額併發槽;FP8 版只剩約 2.2GB ≈ 18K token,滿額槽剩 2 條。
這就是為什麼主角是 8B AWQ——14B 的權重會把 KV 擠到沒地方站,壓測還沒開始就先輸在容量。
這套帳第之後會展開成完整公式。
兩台都能當受測者,方法一模一樣,換的只有 server 那行和 --base-url。
今天先跑 Mac:它的槽數是我自己宣告的,容量線遠遠用不完,正好單獨看吞吐線長什麼樣。雙卡機那輪等模型與顯存的帳都算完再開。
# 受測端:Mac 起 llama-server(8B Q4_K_M)
llama-server -m Llama-3.1-8B-Instruct-Q4_K_M.gguf \
--host 0.0.0.0 --port 8080 -c 32768 -np 4 -fa on --metrics
# -c 是「總 context」,-np 4 均分後每槽 8K
# --metrics 開 Prometheus 端點,壓的時候順便側錄水位
# (雙卡機那輪的起法,一併放這裡備查)
# vllm serve hugging-quants/Meta-Llama-3.1-8B-Instruct-AWQ-INT4 \
# --max-model-len 8192 --gpu-memory-utilization 0.9 --port 8000
# 發射端(跑在受測者對面那台) ── 步驟 ①:先量單流,把容量算出來
vllm bench serve --endpoint-type openai-chat \
--base-url http://<mac-ip>:8080 --endpoint /v1/chat/completions \
--model Llama-3.1-8B-Instruct-Q4_K_M \
--tokenizer NousResearch/Meta-Llama-3.1-8B-Instruct \
--dataset-name random --random-input-len 1024 --random-output-len 256 \
--num-prompts 16 --max-concurrency 1 --request-rate inf --seed 42 --ignore-eos
# 步驟 ②:用單流的 E2E 算網格中心
# 容量上限 = 槽數 ÷ 每請求佔槽時間 = 4 ÷ 5.77s ≈ 0.69 req/s
# ⚠️ 分母要用 E2E,不是「輸出長度 ÷ tok/s」——prefill 那段也佔著槽
# 網格從上限的 1/4 鋪到 4 倍才夾得住膝點
for r in 0.17 0.35 0.7 1.4 2.8; do # ← 換成你自己算出來的中心值
vllm bench serve --endpoint-type openai-chat \
--base-url http://<mac-ip>:8080 --endpoint /v1/chat/completions \
--model Llama-3.1-8B-Instruct-Q4_K_M \
--tokenizer NousResearch/Meta-Llama-3.1-8B-Instruct \
--dataset-name random --random-input-len 1024 --random-output-len 256 \
--num-prompts $(python3 -c "print(max(40,int($r*150)))") \
--request-rate $r --burstiness 1.0 --ignore-eos \
--percentile-metrics ttft,tpot,itl,e2el --metric-percentiles 50,95,99 \
--goodput ttft:2000 tpot:100 --seed 42
done
# num-prompts 跟著 rate 縮放,讓每檔都跑約兩分半——固定 200 發在低 rate 要等 20 分鐘
# 閉環對照:固定 4 條併發打到飽(槽數多少就開多少)
#(同上,把 --request-rate $r 換成 --request-rate inf --max-concurrency 4)
三個我實際踩到的坑,照抄之前先看一眼
--backend 這個參數在新版已經無效,要用 --endpoint-type。寫錯不會報錯,它會默默拿 /v1/completions 的格式去打 chat 端點,然後丟一句沒頭沒尾的 Bad Request。max_completion_tokens,而 Ollama 只認 max_tokens——同一顆模型、同一個 prompt,要求 16 個 token,max_tokens 老實給 16,max_completion_tokens 給了 1004 個。壓測時每個請求都吐到 context 滿為止(我 log 裡看到 n_decoded 衝到一萬三),跑一小時都跑不完,而且 TPOT 全是假的。llama-server 沒這問題。Day 02 說 Ollama 底層是 llama.cpp——底層一樣,但殼不一樣,這就是代價。
meta-llama/... 是 gated repo,--tokenizer 指過去會要你登入;換成 NousResearch/Meta-Llama-3.1-8B-Instruct 這個鏡像,同一份 tokenizer,免登入。先看步驟 ① 在我這台跑出什麼——T1 M4 Max 128GB,llama.cpp(Metal),Llama-3.1-8B-Instruct Q4_K_M,4 槽 × 8K。我跑了三輪(同一組 prompt 重跑一次、換一組 prompt 再跑一次)【實測】:
| 指標 | 三輪的中位 | 三輪全距 |
|---|---|---|
| TTFT p50 | 2.0 s | 1.9 – 2.1 s |
| TPOT p50 | 15.5 ms | 13.7 – 17.9 ms(每流 56 – 73 tok/s) |
| E2E p50 | 5.8 s | 5.3 – 6.6 s |
**先講那個全距,它比中位數更值錢。**同一台機器、同一顆模型、連 prompt 都同一組,重跑一次 TPOT 就差了 12%;換一組 prompt 差到 27%。所以任何人跟你報「這張卡 TPOT 15.51 毫秒」——包括我——你都該先問他跑了幾輪。我上面刻意只寫到小數點後一位,因為第二位在這個雜訊裡沒有意義。這也是 Day 02 那句「跑五次、第一次當暖身、取中位數」的實體版:不是儀式,是因為單跑一次真的會騙你。
E2E 那格就是算容量的分母:4 槽 ÷ 5.8 秒 ≈ 0.7 req/s(三輪落在 0.61 – 0.75,量級穩定,這就夠了——網格只需要對到量級)。
順帶跟上面那張 SLO 表對一次:Anyscale 的示例是 TPOT < 15ms,我這台三輪是 13.7 / 15.5 / 17.9——一輪過、兩輪沒過。同一台機器對同一條線可以給出三個不同答案,這就是為什麼那格寫「示例」而不是「標準」。
掃描那五檔加閉環,我留到 Day 14 一起結帳——因為那天才有對答案的工具:Little's Law 會從紙上算出這台機器該在哪個 rate 崩,掃描表則從實機量出它實際在哪裡崩。兩個數字擺在一起,公式才算被驗過。形狀可以先押:rate 低時 TTFT 平坦,過了某個點 p95 先炸、p50 後炸。
一台跑得動 8B 量化模型的機器當 server(NVIDIA 8GB+ 或 Apple Silicon 16GB+),加上第二台當發射器:Linux/容器直接 pip install vllm,Mac 端裝 vllm-metal 外掛或 GuideLLM。真的只有一台的讀者,同機跑也能練指令,但要知道數字會偏——偏多少,分開跑一次對照就知道。
vllm bench serve 是 load generator,--endpoint-type 加 --base-url 可以壓任何 OpenAI 相容 server;發射器永遠跑在受測者對面那台。--request-rate)量含排隊的真實體感,閉環(--max-concurrency)量極限吞吐;--request-rate 預設 inf 是 time-0 全發,不帶參數的數字不可信。--goodput 把自家 SLO 寫進壓測。尺跟發射器都備好了,下一個問題是往槍口前面站的受測者:模型。明天 Day 04〈讀懂模型名字:30B-A3B 到底是 30B 還是 3B〉——名字裡的每一段都在暗示它跑起來的樣子,讀錯一段,容量帳就會差出十倍。
咱們明天見。