量測推論容量
以固定工作負載、逐類別延遲目標、佇列證據與故障測試量測有效容量。
- 作者
- Huang Tzu LinFounder
- 發布日期
- 閱讀時間
- 約 29 分鐘
- 更新於
推論基準測試要確認的是,版本固定的系統能否持續承受宣告的工作負載,同時讓所需比例的請求符合延遲與可靠性條件。少了工作負載、抵達過程、快取狀態、量測母體與驗收條件,單看每秒 token 數無法回答這個容量問題。
本篇把 I-06 的部署配置轉成維運產物。你會凍結混合工作負載,分別執行飽和測試與速率型測試,計算請求指標與有效吞吐量(goodput),推導保留容量餘裕後的副本需求,定義儀表板,最後注入故障,確認服務會以安全、可觀測的方式失效。
依 SLO 選擇容量
原始吞吐量最高的點可能不合格。
長條高度:成功請求/秒
門檻:每類成功請求的延遲達標率 ≥95%,且成功樣本 ≥2,000;整體錯誤率 ≤1%;佇列穩定。這些門檻並未限制各類別的錯誤率。
每類最少成功樣本數:2191。長條高度是每秒成功請求數,下方是提供速率;此摘要不能取代各類別尾端延遲與重複執行驗證。
讀懂目前結果
- 提供 → 成功請求/秒
- 24 → 23.9
- 最低類別延遲達標率 ≥95%
- 95.1%
- 最少類別成功樣本 ≥2,000
- 2191
- 整體錯誤 ≤1%;佇列穩定
- 0.4% · 穩定
28 RPS 每秒成功完成的請求較多,卻未通過延遲、整體錯誤率與佇列門檻。此處延遲達標率只計成功請求。這四個示意點中最高通過的提供速率為 24 RPS;未測試範圍的容量仍未知。
本文的示意資料點;容量判斷另需足夠樣本與可重複的佇列證據。
本篇如何銜接前後內容
本篇在學習路線中的位置
帶入前作:I-01:token 預算排程追蹤、I-02:KV 區塊容量計算表、I-03:等量 GPU 部署配置與 KV 傳輸基準測試計畫、I-06:平行化計畫與集體通訊成本估算
本篇新增:推論容量資料包與故障測試紀錄。請將假設、來源 ID 與失敗案例連同成果一併留存。
後續用途:用這份產物檢視自己的實作,並記錄尚待釐清的假設。跨主題系列的先備順序請見完整閱讀路線。文章 ID 是固定標籤,編號不代表閱讀順序。
學習目標與實作準備
開始之前:先備知識與學習成果
你應該已理解 TTFT、ITL、TPOT、端對端延遲、連續批次處理、KV 快取,以及 I-06 建立的 TP2 × DP4 部署配置。
完成後,你將能夠:
區分閉迴路並行測試與開迴路速率測試;
凍結工作負載分布,而不是只測一種方便的 prompt 形狀;
分開呈現冷快取與暖快取的結果;
計算 TTFT、ITL、TPOT、端對端延遲、吞吐量、觀測 goodput 與 SLO 合格容量;
從實測容量與預留比例推導副本及 GPU 數量;
選擇請求、佇列、KV、GPU、網路與故障指標;以及
證明警示與復原流程在故障注入下確實有效。
你將建置的成品
你會產出版本化的 inference-capacity-pack/,內含四個可供檢查的檔案:
inference-capacity-pack/
├── workload-manifest.yaml
├── requests.csv
├── slo-report.json
└── failure-test.mdworkload-manifest.yaml 凍結系統與流量;requests.csv 每一列是一筆排定請求及其最終狀態;slo-report.json 收錄百分位數、錯誤率、SLO 達標率與 goodput;failure-test.md 記錄一次刻意注入的故障、它觸發的警示與復原時間。
凍結服務系統
完整範例採用以下假設性的規劃條件:
輸入 | 假設 |
|---|---|
模型 |
|
部署配置 | 4 個副本,每個副本 TP2,共 8 張 GPU |
引擎與 tokenizer |
|
串流 | 啟用 |
前綴快取 | 啟用,但冷快取與暖快取測試分開執行 |
推測式解碼 | 停用 |
P/D 分離 | 停用 |
量測點 | 從用戶端送出到用戶端接收 |
主要 SLO 達標目標 | 每個類別各自計算,至少 95% 的成功請求同時符合單筆請求的 TTFT 與 TPOT 門檻 |
最大錯誤率 | 1% |
容量預留 | 運作時不超過實測 SLO 合格容量的 75% |
引擎與 tokenizer 的版本必須固定,因為指標行為與 token 數都可能隨版本改變。vLLM 的基準測試文件也提醒,各家基準測試工具的指標術語並未標準化,所以回報指標名稱時,要一併附上公式與量測點(vLLM Benchmark CLI)。
凍結工作負載分布
只用單一平均輸入長度,會抹平 prefill 為主與 decode 為主兩類流量的差異。請改為明確定義類別:
類別 | 比例 | 輸入 token 數 | 輸出 token 數 | 單筆 TTFT 門檻 | 單筆 TPOT 門檻 |
|---|---|---|---|---|---|
interactive | 70% | 1,024 | 128 | 500 ms | 40 ms/token |
long_context | 20% | 8,192 | 256 | 2,500 ms | 50 ms/token |
generation_heavy | 10% | 512 | 1,024 | 800 ms | 45 ms/token |
依這組固定比例計算:
weighted input tokens
= 0.70×1,024 + 0.20×8,192 + 0.10×512
= 2,406.4 tokens/request
weighted output tokens
= 0.70×128 + 0.20×256 + 0.10×1,024
= 243.2 tokens/request加權值可用來核對整體計數器,但每個類別的 SLO 達標率仍須各自評估。整體百分位數或達標率看起來正常時,流量較小的類別(例如 long-context)仍可能未達標。
工作負載清單(manifest)還應凍結取樣參數、停止條件、對話範本(chat template)、前綴分布、請求取消策略、測試時長、用戶端位置、確切的模型與引擎設定,以及隨機種子或請求排程。抵達模型本身就是實驗條件,不是可以藏起來的用戶端細節。以 NVIDIA AIPerf 為例,文件中的速率型測試可選 constant、Poisson 或 gamma 抵達模式,未設定速率時則採並行突發(concurrency burst)模式,另外也支援按固定排程重播請求追蹤紀錄(AIPerf load-generator options)。
閉迴路與開迴路回答不同問題
閉迴路並行測試
閉迴路測試最多同時維持 N 筆進行中的請求,一筆完成後,用戶端才送出下一筆。延遲上升時,完成速度變慢,新請求的抵達也跟著變慢。這種測試適合找出飽和點,以及吞吐量與延遲之間的權衡前緣,但它會自我節流,因此無法證明服務撐得住與完成速度無關的正式環境抵達率。
AIPerf 把只設定並行數的執行方式稱為突發(burst)模式:請求之間沒有延遲,只受並行上限限制;文件也把飽和測試列為它的用途之一(AIPerf arrival patterns)。
開迴路速率測試
開迴路測試依目標速率排定抵達時間,與完成速度無關,常用 Poisson 過程或由追蹤紀錄(trace)推導的抵達過程。送入負載一旦超過服務容量,佇列就會成長,或由有界准入拒絕工作;這時 TTFT 與拒絕、逾時計數會一起顯示超載。要驗證外部抵達率 SLO,就該用這種測試,前提是負載產生器不會因為並行上限,悄悄把已排定的抵達擋下來。例如 AIPerf 在達到設定的並行上限時,就會阻塞依速率排定的請求(AIPerf load-generator options)。
兩種測試都要做;手上有正式環境資料時,再加上 trace 重播:
以閉迴路並行數掃描找出飽和候選點;
以開迴路速率掃描,驗證貼近實際的抵達率下,佇列是否穩定、SLO 是否達標;以及
有正式環境的抵達與長度分布時,重播帶時間戳記的 trace。
在測試前先定義指標
設請求 i 的用戶端送出時間為 s_i、收到第一個非空 token 的時間為 f_i、收到最後一個 token 的時間為 e_i,輸出 token 數為 O_i:
TTFT_i = f_i - s_i
E2E_i = e_i - s_i
TPOT_i = (E2E_i - TTFT_i) / (O_i - 1), for O_i > 1若各 token 的抵達時間戳記為 t_i,1 ... t_i,O_i,個別的 token 間延遲(inter-token latency)為:
ITL_i,j = t_i,j - t_i,j-1, for j = 2 ... O_i上述 token 間隔公式假設每個可見 token 都有各自觀測到的抵達時間。若單一串流片段(chunk)含有多個 token,請回報實際觀測到的 chunk 間隔,不要自行虛構每個 token 的時間戳記。這裡的 E2E 止於最後一個可見 token,不含尾端的協定中繼資料;所有時間戳記都取自同一個用戶端單調時鐘。輸出只有零或一個 token 時,TPOT 沒有定義;本範例採固定輸出長度,每個類別都會產生不只一個 token。
TPOT 是每筆請求算出的單一平均值,ITL 則記錄每一段間隔。若每個 token 都以獨立且帶時間戳記的事件抵達,TPOT 就等於該筆請求所有 ITL 的算術平均,但平均值可能掩蓋一次長時間停頓。若一個串流 chunk 帶有多個 token(推測式解碼就可能如此),chunk 間隔與每 token 平均值的分母就不同。比較指標之前,先核對公式與量測點(vLLM Benchmark CLI)。
設量測區間長度為 T:
request_throughput = successful_requests / T
output_throughput = sum(successful_output_tokens) / T
error_rate = failed_requests / attempted_requests
observed_goodput = successful_requests_meeting_their_class_request_cutoffs / T
class_attainment_c = passing_successful_requests_in_class_c
/ successful_requests_in_class_c
all_class_SLOs_pass = AND over every class c of:
class_attainment_c >= 0.95
AND successful_requests_in_class_c >= 2,000本篇所說的SLO 合格送入容量(SLO-compliant offered capacity),是指已測試的送入請求速率中,服務同時滿足以下條件的最高值:
每個工作負載類別各自計算,成功請求的單筆聯合延遲達標率至少 95%;
每個類別都有至少 2,000 筆成功觀測;
以所有嘗試請求計算的錯誤率不超過 1%;以及
穩態區間內,佇列沒有持續成長。
整體的觀測 goodput 仍有參考價值,但只要有一個類別沒通過延遲閘門或樣本數閘門,整體數字就救不了這個測試點。延遲達標率的分母排除錯誤,錯誤率上限卻以整體計算,兩者合用會留下盲點。以固定 70%/20%/10% 的工作負載為例:共 24,000 次嘗試,240 次錯誤全落在最小類別,整體錯誤率是 1%,但該類別的成功率只有 90%。如果這 2,160 筆成功請求全都符合延遲門檻,這個測試點就會通過本篇宣告的閘門。若需求是逐類別的可用率,或要以全部嘗試請求計算延遲達標率,就要另外加上逐類別錯誤率閘門,或以全部嘗試為分母的達標率閘門。
DistServe 把 goodput 定義為:在 TTFT 與 TPOT 都達到 SLO 達標率目標的前提下,每張投入的 GPU 能服務的最高請求速率。這是容量指標,比較接近把本篇的 SLO 合格容量換算到每張 GPU 的值,而不是前面以量測區間計算的觀測 goodput,因此回報 goodput 時要講明採用哪一種定義。DistServe 報告的改善幅度,也只適用於它評估的模型、工作負載與 SLO(Zhong et al., OSDI 2024)。
控制快取狀態,不要把它平均掉
請分成兩組各自命名的實驗:
冷快取
每個測試點開始前,重啟引擎或明確清除其快取;
若要整段測試都維持冷快取,就必須同時防止測試內與跨測試的前綴重用,做法是讓每筆請求開頭的前綴都不同,或使用引擎支援的逐請求隔離;
穩態區間排除編譯與模型載入的暖機時間;以及
回報接近零的前綴快取命中率。
暖快取
先用一組版本固定的重複前綴暖機;
不同設定之間維持相同的前綴頻率分布;
預期的快取狀態成立後才開始量測;以及
回報前綴快取命中率與驅逐率。
若 prompt 共用同一段系統前綴,只清除一次快取得到的是冷啟動,而不是持續維持冷快取的工作負載;這段暖機軌跡要另外回報。絕對不要把冷快取與暖快取的請求混成一個未標示的平均值。vLLM 的基準測試指南也提醒,對同一台伺服器重跑可重現的資料集,可能重用前綴快取裡殘留的 prompt,讓吞吐量虛高(vLLM Benchmark CLI)。
完整範例:找出 SLO 合格容量
假設每個測試點都使用固定種子的 Poisson 抵達排程,共 22,000 筆請求,洗牌前先分層為恰好 15,400 筆 interactive、4,400 筆 long-context 與 2,200 筆 generation-heavy。每點至少量測五分鐘;若任一類別跑完後的成功觀測少於 2,000 筆,就沿用事先承諾的排程繼續執行。到達宣告的最長測試時間就停止,仍然樣本不足的點標為無法判定,以免某個完全失敗的類別讓測試永遠跑不完。快取策略維持不變。以下是參數化教學資料,不是在 OptiVerse 基礎設施上的量測結果:
送入 RPS | 成功 RPS | 錯誤率 | 整體達標率 | 最低類別達標率 | 最低類別成功筆數 | 觀測 goodput RPS | 輸出 tok/s | Interactive P95 TTFT | Interactive P95 TPOT | 佇列趨勢 |
|---|---|---|---|---|---|---|---|---|---|---|
16 | 16.0 | 0.0% | 99.6% | 99.1% | 2,200 | 15.94 | 3,891 | 180 ms | 24 ms | 平坦 |
20 | 19.98 | 0.1% | 99.1% | 98.2% | 2,197 | 19.80 | 4,859 | 260 ms | 29 ms | 平坦 |
24 | 23.9 | 0.4% | 95.4% | 95.1% | 2,191 | 22.80 | 5,813 | 420 ms | 38 ms | 平坦 |
28 | 25.2 | 10.0% | 86.8% | 78.0% | 2,160 | 21.87 | 6,129 | 980 ms | 62 ms | 成長 |
在 28 RPS 時,10% 的錯誤會讓最小類別在初始 22,000 筆排程中只剩 1,980 筆成功請求,因此排程延長到 24,000 次嘗試,保留 2,160 筆成功觀測。表中的速率與輸出計數是四捨五入後的示意值,並假設各類別的錯誤大致按比例分布;這不是實際執行紀錄。
28 RPS 的原始輸出吞吐量最高,觀測 goodput 卻低於 24 RPS。它最弱類別的達標率只有 78.0%,interactive 的 P95 TTFT 與 P95 TPOT 都超過該類別門檻,錯誤率過高,佇列也在成長。只看原始吞吐量,就會選錯操作點。
在四個副本下,24 RPS 是已測試送入速率中符合以下條件的最高點:每個類別的成功請求至少 95% 達到延遲門檻,且各有至少 2,000 筆成功觀測,同時整體錯誤率不超過 1%、佇列保持平坦。最弱類別在 2,191 筆成功請求中的延遲達標率為 95.1%。因此:
illustrative SLO-compliant capacity per replica
= 24 RPS / 4 replicas
= 6 RPS/replica只有在工作負載比例、部署配置、快取狀態、引擎設定與擴展條件都相同時,這個除法才成立;它不是適用於所有情況的每副本常數。
保留餘裕的容量規劃範例
假設預測尖峰為 36 RPS,政策預留 25% 容量,因此規劃利用率最多 75%:
required_replicas
= ceil(peak_RPS / (capacity_per_replica × target_utilization))
= ceil(36 / (6 × 0.75))
= ceil(8)
= 8 replicas
required_GPUs = 8 replicas × 2 GPUs/replica = 16 GPUs目前的八 GPU 叢集只有四個副本,依 75% 政策只能承擔 18 RPS,無法在指定的預留比例下撐住 36 RPS 尖峰。這個計算本身不足以核准採購,必須先驗證八副本拓撲,因為負載平衡器行為、網路路徑、快取局部性與共用瓶頸,都可能讓擴展無法呈線性。Dynamo 的容量估算指南也劃出同一條界線:AIConfigurator 是分析型效能模型,會提出候選組態,但不模擬逐請求的排程器或 KV 快取行為,所以仍須對實際端點做基準測試(Dynamo sizing with AIConfigurator)。
在穩定的量測區間內,可以用 Little's Law 檢查各項量測是否一致:
average_in_flight ≈ completion_rate × average_E2E_seconds以下例子與完整範例的工作負載無關:某個穩定區間每秒有 23.9 筆請求進入最終狀態(含所有狀態),平均需要 1.8 秒才進入最終狀態,則預期平均進行中請求數為 23.9 × 1.8 = 43.02。完成計數、平均停留時間與進行中請求數的 gauge 必須涵蓋同一個母體,包括逾時與失敗;只用成功請求的 E2E,無法解釋同時計入失敗請求的 gauge。若這個估計值與觀測到的 gauge 平均值差距很大,可能是量測邊界不一致、遙測資料遺失、重試,或佇列不穩定。Little's Law 救不了持續膨脹的超載佇列,因為穩定系統的前提已經不成立。
建立工作負載清單
建立 inference-capacity-pack/workload-manifest.yaml:
cutoff: 2026-09-11
model:
id: dense-70b-class
revision: dense-70b-fixture-r1
tokenizer_revision: dense-70b-tokenizer-fixture-r1
precision: bf16
engine:
name: tutorial-engine-fixture
version: "1.0"
placement: {tp: 2, pp: 1, dp: 4, replicas: 4, gpus: 8}
prefix_cache: enabled
speculative_decoding: disabled
disaggregated_prefill: disabled
client:
location: loadgen-fixture-a/zone-a
streaming: true
arrival_pattern: poisson
minimum_duration_seconds: 300
initial_scheduled_requests: 22000
request_schedule_seed: 20260911
class_schedule_counts: {interactive: 15400, long_context: 4400, generation_heavy: 2200}
warmup_seconds: 60
maximum_duration_seconds: 3600
request_timeout_seconds: 120
automatic_retries: false
workload:
- {name: interactive, mix: 0.70, input_tokens: 1024, output_tokens: 128,
request_ttft_cutoff_ms: 500, request_tpot_cutoff_ms: 40}
- {name: long_context, mix: 0.20, input_tokens: 8192, output_tokens: 256,
request_ttft_cutoff_ms: 2500, request_tpot_cutoff_ms: 50}
- {name: generation_heavy, mix: 0.10, input_tokens: 512, output_tokens: 1024,
request_ttft_cutoff_ms: 800, request_tpot_cutoff_ms: 45}
acceptance:
class_slo_attainment_min: 0.95
min_successful_samples_per_class: 2000
error_rate_max: 0.01
queue_trend_required: flat
target_utilization: 0.75request_*_cutoff_ms 欄位套用在每一筆請求上;P50/P95/P99 是要回報的分布統計量,不能拿來取代這些門檻。先產生精確的各類別筆數,以事先記錄的種子洗牌,再依 Poisson 過程抽出服從指數分布的抵達間隔。初始排程跑完後,若任一類別的成功列少於 2,000 筆,就從同一份版本化排程補送請求,之後才做容量決策。
使用 AIPerf 時,可用 --request-rate、--arrival-pattern、--streaming、--benchmark-duration 與凍結的資料集表達速率掃描;只設定並行數的掃描,則應另外命名為飽和實驗。AIPerf 的 CLI 參考文件同時列出這兩種控制,預設的匯出層級也會寫出逐請求紀錄,供後續分析(AIPerf command-line options)。
建立 SLO 分析器
把負載測試工具的逐請求輸出正規化成 requests.csv,欄位如下:
request_id,class,ttft_ms,tpot_ms,e2e_ms,output_tokens,status
r-0001,interactive,182.0,25.1,3369.7,128,ok
r-0002,long_context,1940.0,44.2,13211.0,256,ok
r-0003,generation_heavy,910.0,47.0,48991.0,1024,ok
r-0004,interactive,0,0,0,0,error將以下內容存為 analyze_slo.py:
import csv
import json
import math
import sys
REQUEST_CUTOFFS = {
"interactive": {"ttft_ms": 500, "tpot_ms": 40},
"long_context": {"ttft_ms": 2500, "tpot_ms": 50},
"generation_heavy": {"ttft_ms": 800, "tpot_ms": 45},
}
CLASS_ATTAINMENT_MIN = 0.95
MIN_SUCCESSFUL_PER_CLASS = 2000
ERROR_RATE_MAX = 0.01
def percentile(values, probability):
if not values:
return None
ordered = sorted(values)
index = max(0, math.ceil(probability * len(ordered)) - 1)
return ordered[index]
if len(sys.argv) != 4:
raise SystemExit("usage: analyze_slo.py REQUESTS_CSV DURATION_SECONDS QUEUE_TREND")
path = sys.argv[1]
duration_s = float(sys.argv[2])
queue_trend = sys.argv[3]
if not math.isfinite(duration_s) or duration_s <= 0:
raise ValueError("duration must be positive")
if queue_trend not in {"flat", "growing"}:
raise ValueError("queue trend must be flat or growing")
with open(path, newline="", encoding="utf-8") as handle:
rows = list(csv.DictReader(handle))
expected_outputs = {"interactive": 128, "long_context": 256, "generation_heavy": 1024}
seen_ids = set()
for row in rows:
rid = row["request_id"]
if not rid or rid in seen_ids:
raise ValueError("request IDs must be nonempty and unique")
seen_ids.add(rid)
if row["class"] not in REQUEST_CUTOFFS:
raise ValueError("unknown workload class")
if row["status"] not in {"ok", "error", "timeout", "rejected", "cancelled"}:
raise ValueError("every scheduled request needs a recognized terminal status")
attempted = len(rows)
successful = [row for row in rows if row["status"] == "ok"]
failed = attempted - len(successful)
passed_total = 0
by_class = {
name: {"ttft": [], "tpot": [], "e2e": [], "tokens": 0, "passed": 0}
for name in REQUEST_CUTOFFS
}
for row in successful:
name = row["class"]
if name not in REQUEST_CUTOFFS:
raise ValueError(f"unknown workload class: {name}")
ttft = float(row["ttft_ms"])
tpot = float(row["tpot_ms"])
e2e = float(row["e2e_ms"])
output_tokens = int(row["output_tokens"])
if not all(math.isfinite(x) and x >= 0 for x in (ttft, tpot, e2e)):
raise ValueError("latencies must be finite and nonnegative")
if output_tokens != expected_outputs[name]:
raise ValueError("successful output length differs from the fixed workload")
if abs(e2e - (ttft + tpot * (output_tokens - 1))) > 0.5:
raise ValueError("TTFT/TPOT/E2E disagree under this fixture's metric convention")
bucket = by_class[name]
bucket["ttft"].append(ttft)
bucket["tpot"].append(tpot)
bucket["e2e"].append(e2e)
bucket["tokens"] += output_tokens
cutoff = REQUEST_CUTOFFS[name]
if ttft <= cutoff["ttft_ms"] and tpot <= cutoff["tpot_ms"]:
bucket["passed"] += 1
passed_total += 1
classes = {}
for name, cutoff in sorted(REQUEST_CUTOFFS.items()):
bucket = by_class[name]
count = len(bucket["ttft"])
attainment = bucket["passed"] / count if count else None
sample_adequate = count >= MIN_SUCCESSFUL_PER_CLASS
p99_sample_adequate = count >= 10000
class_slo_pass = sample_adequate and attainment >= CLASS_ATTAINMENT_MIN
classes[name] = {
"successful_count": count,
"passing_count": bucket["passed"],
"request_ttft_cutoff_ms": cutoff["ttft_ms"],
"request_tpot_cutoff_ms": cutoff["tpot_ms"],
"attainment": attainment,
"sample_adequate": sample_adequate,
"p99_sample_adequate": p99_sample_adequate,
"class_slo_pass": class_slo_pass,
"p50_ttft_ms": percentile(bucket["ttft"], 0.50),
"p95_ttft_ms": percentile(bucket["ttft"], 0.95),
"p99_ttft_ms": percentile(bucket["ttft"], 0.99) if p99_sample_adequate else None,
"p50_tpot_ms": percentile(bucket["tpot"], 0.50),
"p95_tpot_ms": percentile(bucket["tpot"], 0.95),
"p99_tpot_ms": percentile(bucket["tpot"], 0.99) if p99_sample_adequate else None,
"p95_e2e_ms": percentile(bucket["e2e"], 0.95),
"output_tokens": bucket["tokens"],
}
error_rate = failed / attempted if attempted else None
overall_attainment = passed_total / len(successful) if successful else None
all_class_slos_pass = all(item["class_slo_pass"] for item in classes.values())
capacity_gate_pass = (
all_class_slos_pass
and error_rate is not None
and error_rate <= ERROR_RATE_MAX
and queue_trend == "flat"
)
report = {
"attempted": attempted,
"successful": len(successful),
"failed": failed,
"error_rate": error_rate,
"overall_attainment": overall_attainment,
"observed_goodput_rps": passed_total / duration_s,
"request_throughput_rps": len(successful) / duration_s,
"output_throughput_tps": sum(int(row["output_tokens"]) for row in successful) / duration_s,
"queue_trend": queue_trend,
"all_class_slos_pass": all_class_slos_pass,
"capacity_gate_pass": capacity_gate_pass,
"classes": classes,
}
print(json.dumps(report, indent=2, sort_keys=True))執行:
python analyze_slo.py inference-capacity-pack/requests.csv 300 flat \
> inference-capacity-pack/slo-report.json請採用封閉群組(closed cohort):納入每一筆排定請求,確認每筆都有最終狀態,並把最後的排空或逾時時間算進 DURATION_SECONDS。上面指令裡的 300 只適用於四列的快速初篩測試(smoke test);正式測試點要傳入整個群組的實際時長。本範例停用自動重試;若啟用重試,就必須分開記錄邏輯請求與實際嘗試。請拿設定的抵達率對照實際發送的時間戳記,並回報用戶端排程延遲。分析器只依你提供的資料列與時長計算速率,無法確認匯出檔包含每一次嘗試,也無法驗證你傳入的佇列標記是否屬實,因此請保留原始 trace 與佇列時間序列。
上方四列 CSV 只是檢查欄位結構的 smoke test,各類別樣本刻意不足,capacity_gate_pass 必定為 false。正式測試點要通過分析器的初步容量閘門,必須同時滿足:每個命名類別都有至少 2,000 筆成功列,且成功請求的單筆聯合延遲達標率至少 95%;整體錯誤率不超過 1%;佇列趨勢為 flat。只有用戶端排程積壓與排程器佇列深度兩條時間序列都沒有持續成長時,才能傳入 flat。在把某個測試點認定為容量之前,還要用排程核對 CSV 中的 ID、確認流量組成與執行設定,並檢視重複測試的結果。這支程式以 nearest-rank 法計算百分位數,結果可以重現;正式環境的工具可能改用內插,因此要記錄所用的演算法,不要只看百分位數名稱就直接比較。
可觀測性:量測工作、狀態與故障
正式環境的儀表板要有足夠的訊號,才能解釋 SLO 為什麼改變。維度請選基數有界的欄位,例如模型版本、引擎版本、工作負載類別、副本與結果。請求 ID 應放在追蹤與紀錄裡,不要當成指標標籤。
請求與 SLO 訊號
嘗試、成功、失敗、逾時、遭拒與取消的請求數;
依工作負載類別區分的 TTFT、TPOT、ITL 與 E2E 直方圖;
輸入與輸出 token 數,以及請求吞吐量與輸出吞吐量;
觀測 goodput 與各類別的 SLO 達標率;以及
重試次數與用戶端斷線次數。
排程器與佇列訊號
等待中與執行中的請求數;
佇列等待時間直方圖;
批次內的 prompt token 與生成 token 數;
搶占、驅逐與准入拒絕次數;以及
引擎支援時,每次迭代的批次組成。
KV 與 GPU 訊號
已用與總計 KV 區塊數,以及最大 token 容量;
前綴快取命中率與驅逐率;
HBM 用量、運算使用率、記憶體頻寬、功耗與 OOM 事件;以及
模型載入與暖機時間。
分散式訊號
依操作類型與訊息大小分桶的集體通訊耗時與錯誤;
分離式服務的 KV 傳輸位元組數、延遲、吞吐量與失敗次數;
各副本進行中的工作量與路由選擇結果;以及
啟用推測式解碼時的接受長度與接受率。
NVIDIA Dynamo 的引擎指標比較文件列出 vLLM、SGLang 與 TensorRT-LLM 提供的 Prometheus 指標,並提醒指標名稱與數量會隨引擎版本改變。請直接檢查實際部署的 /metrics 端點,不要原封不動沿用舊儀表板(Dynamo Engine Metrics Comparison)。
刻意破壞的基準測試
這組設定能跑出亮眼的數字,卻幾乎提供不了維運證據:
load_mode: concurrency_only
concurrency: 128
request_count: 100
prompts: repeat_the_same_prompt
prefix_cache: enabled
clear_cache_before_run: false
warmup_requests: 0
report: [mean_latency, total_tokens_per_second]
model_revision: latest
engine_version: latest問題出在:
只設定並行數的流量會自我節流,無法驗證與完成速度無關的抵達率;
100 筆請求幾乎提供不了尾端證據;
重複的 prompt 加上未清除的快取,會把冷快取與暖快取狀態混在一起;
平均值會掩蓋尾端延遲與類別失敗;
報告裡沒有錯誤、佇列成長、取消與快取狀態;以及
會變動的
latest版本讓結果無法重現。
多加一個百分位數修不好它。請改用版本固定的清單、分開的快取狀態、兩種負載模式、逐請求紀錄、各類別 SLO 與故障測試。
故障注入:證明儀表板有效
在隔離的測試部署中,讓服務跑在已通過驗收的 20 RPS 測試點,接著終止四個 TP2 副本中的一個,並在 failure-test.md 記錄:
injection_time
detection_time
router_removal_time
first_error_time and last_error_time
TTFT/TPOT/SLO-attainment during incident
queue peak and recovery time
requests retried, rejected, or lost
which alerts fired
whether capacity returned without manual data repair別期待「完全沒有變化」:少了 25% 的副本,容量本來就該下降。系統要通過測試,必須能偵測並移除故障副本、不再把新工作送過去、如實呈現錯誤或背壓,最後回到穩定狀態。依線性估計,少一個副本後只剩 18 RPS 的容量,若依 75% 利用率政策則只有 13.5 RPS。如果剩下的三個副本如估計所示,無法在 SLO 內撐住 20 RPS,准入控制就應拒絕請求或卸除負載,而不是放任佇列無限成長。
之後再分別演練冷啟動的自動擴展副本、縮減的 KV 容量、請求取消,以及效能劣化的跨節點連結。這些測試針對不同的故障機制,請分開進行,不要壓縮成單一的混沌分數。
動手實作
產生或擷取符合清單中三個類別設定的工作負載。
執行閉迴路並行數掃描,找出飽和點。
以固定種子的 22,000 筆請求排程,在 16、20、24 與 28 RPS 分別執行冷快取與暖快取的 Poisson 速率掃描,每點至少五分鐘。
把每一筆嘗試的請求正規化寫入
requests.csv;持續執行排程,直到每個類別都有至少 2,000 筆成功列。產生
slo-report.json,並以送入 RPS 為橫軸,畫出原始吞吐量、觀測 goodput、各類別達標率、P95 TTFT 與 P95 TPOT。核對排程完整性、工作負載是否忠實、快取狀態、佇列證據與重複測試間的不確定性後,選出分析器通過的最高已測速率。
以 75% 目標利用率,計算 36 RPS 所需的副本與 GPU 數。
在已通過驗收的測試點注入單一副本故障,並完成
failure-test.md。
若某類別的成功樣本少於 2,000 筆,請把它的尾端結果標為探索性,也不得把該測試點選為容量。分層排程保證占 10% 的類別一開始有 2,200 次嘗試,分析器仍會在扣除錯誤後檢查成功筆數。2,000 與 10,000 這兩個樣本門檻是本教學的回報政策,不是統計保證。10,000 筆觀測中只有約 100 筆落在最高的 1%,自相關與突發抵達還會進一步縮小有效樣本數。請回報重複測試之間的變異,以及有依據的不確定性估計。這支程式在成功觀測少於 10,000 筆時不回報 P99,但超過這個門檻也不代表 P99 的精度已獲保證。
若只做計算器練習,請執行四列 smoke test,預期結果為 attempted=4、successful=3、failed=1、error_rate=0.25、passing=2、observed_goodput=2/300 RPS,以及 capacity_gate_pass=false。沒有硬體的練習可以驗證分析流程並寫出故障測試計畫,但不能宣稱已量測容量或已成功復原。
可量測的完成標準
容量資料包在以下條件全部成立時才算通過:
模型、tokenizer、引擎、精度、排程器與工作負載的版本都固定不變;
冷快取與暖快取測試分別命名,並回報命中與驅逐狀態;
同時包含閉迴路飽和測試與開迴路抵達率掃描;
每個測試點都回報嘗試、成功與失敗的請求數、錯誤率、P50/P95 延遲、吞吐量、goodput、佇列趨勢,以及各類別的成功筆數;
每個類別都各自依單筆請求的 TTFT/TPOT 門檻評估,且至少有 2,000 筆成功觀測;
選定的容量點上,每個類別成功請求的單筆聯合延遲達標率至少 95%,整體錯誤率最多 1%,佇列也沒有持續成長;
副本計算納入 75% 利用率政策,並以範例輸入算出 8 個副本、16 張 GPU;
故障測試記錄了偵測與復原時間,且至少有一個已設定的警示觸發;
儀表板能區分佇列飽和、KV 壓力、GPU 故障與網路/傳輸劣化;以及
重複測試會揭露變異,不會只發布最好的一次。
提取練習
為什麼閉迴路測試可能隱藏超載?
TPOT 與 ITL 有什麼不同?
為什麼冷快取與暖快取的結果必須分開?
觀測 goodput 和原始請求吞吐量有什麼不同?
在完整範例中,為什麼選 24 RPS 的送入速率,而不是 28?
在指定預留比例下,36 RPS 需要多少副本與 GPU?
哪些訊號能區分佇列問題與 KV 記憶體壓力?
故障測試能證明哪些事,是只對健康系統做的基準測試證明不了的?
解答
新工作只在先前工作完成後才送出,因此延遲升高會減慢抵達,讓用戶端自我節流。
TPOT 是每筆請求單一的平均 decode 時間;ITL 則包含串流輸出之間每一段實際觀測到的間隔。
前綴重用會減少 prefill 工作量,讓吞吐量虛高;沒交代快取策略與比例就混合兩種狀態,工作負載的意義就說不清。比例固定且有標示的混合狀態,則可以當成獨立的實驗來評估。
它只計算單位時間內符合所屬類別單筆門檻的成功請求;容量則另外要求每個類別都通過各自的達標率與樣本數閘門。
28 RPS 的原始吞吐量較高,但最弱類別的達標率只有 78.0%,錯誤率是 10.0%,佇列也在成長。24 RPS 時,每個類別都以至少 2,191 筆成功請求達到至少 95.1% 的達標率,其他閘門也都通過。
ceil(36/(6×0.75)) = 8個副本;每個副本 TP2,因此共 16 張 GPU。佇列深度與等待時間、准入指標會顯示飽和;KV 使用率、可用區塊、驅逐與搶占則顯示 KV 壓力。GPU 與請求指標可作為佐證。
它證明在已知故障下,偵測、從路由移除、卸除負載與重試行為、警示與復原流程都能運作。
本文不涵蓋的內容
本篇定義的是推論維運方法,不是通用的 SLO。產品團隊必須依使用者需求與成本、風險限制推導目標。參數化結果不是 OptiVerse 的實測結果,線性副本計算也必須在水平擴展後驗證。即使基礎設施 SLO 通過,品質評估、模型準確度、安全性與應用程式正確性仍是各自獨立的閘門。
來源與延伸閱讀
vLLM Project. "Benchmark CLI." 現行文件,查閱日期 2026-11-02。用戶端觀測的 TTFT、ITL、TPOT 與吞吐量定義,以及快取暖機警告的第一方說明。docs.vllm.ai
NVIDIA. "AIPerf Load Generator Options Reference." 現行文件,查閱日期 2026-11-02。速率、並行數、trace 與使用者驅動等負載排程方式的第一方定義。docs.nvidia.com/aiperf
NVIDIA. "AIPerf Command Line Options." 現行文件,查閱日期 2026-11-02。請求速率掃描、Poisson/gamma/constant 抵達模式、並行數、暖機、測試時長與請求取消的第一方控制說明。docs.nvidia.com/aiperf
Zhong, Y., Liu, S., Chen, J., et al. "DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving." OSDI, 2024。LLM 服務中以 SLO 為條件定義 goodput 的第一手來源。usenix.org
NVIDIA Dynamo. "Engine Metrics Comparison." 現行文件,查閱日期 2026-11-02。涵蓋請求、佇列、KV、路由、傳輸與推測式解碼指標的第一方跨引擎清單。docs.nvidia.com/dynamo
NVIDIA Dynamo. "Size a Local Deployment with AIConfigurator." 現行文件,查閱日期 2026-11-02。先估算候選部署配置,再對實際端點驗證的第一方指南。docs.nvidia.com/dynamo
NVIDIA. "LLM Inference Benchmarking: Fundamental Concepts." 2025 年 4 月。指標定義、工作負載長度,以及吞吐量與延遲取捨的第一方說明。developer.nvidia.com
閱讀順序
完整閱讀路線
章節 25 / 25
上一章
部署混合專家模型
下一章
尚無可用文章
Huang Tzu Lin
富宇資訊科技創辦人。撰寫 AI 系統設計、證據與工程取捨的文章。
