跳至主要內容
推論基礎設施

推論基準測試、容量、SLO 與可觀測性

作者
Huang Tzu LinFounder
發布日期
閱讀時間
約 21 分鐘
教學路徑

完成本篇後,你應該能夠

  • 區分閉迴路 concurrency 測試與開迴路 request-rate 測試;
  • 凍結工作負載分布,而不是只測一種方便的 prompt 形狀;
  • 分開 cold-cache 與 warm-cache 結果;
  • 計算 TTFT、ITL、TPOT、端對端延遲、吞吐量、觀測 goodput 與符合 SLO 的容量;
  • 從實測容量與預留比例推導副本及 GPU 數量;
  • 選擇請求、佇列、KV、GPU、網路與故障指標;以及
  • 證明警示與復原流程在故障注入下確實有效。

完成檢核你能逐項滿足下列所有完成標準嗎?

完整完成標準10
  1. model、tokenizer、engine、precision、scheduler 與 workload revision 都不可變;
  2. cold/warm cache 測試分別命名,並回報 hit/eviction 狀態;
  3. 同時包含閉迴路 saturation 與開迴路 arrival-rate sweep;
  4. 每個測試點都回報 attempted/completed/failed requests、error rate、P50/P95 latency、throughput、goodput、queue trend 與逐類別成功筆數;
  5. 每個類別都分別依 request-level TTFT/TPOT cutoff 評估,且至少有 2,000 筆成功觀測;
  6. 選定容量點的每個類別都至少有 95% joint request-level 達成率、最多 1% 錯誤,且沒有持續 queue growth;
  7. 副本計算包含 75% utilization 政策,並對範例輸入得到 8 replicas/16 GPUs;
  8. failure test 記錄 detection/recovery time,而且至少一個已配置 alert 觸發;
  9. 儀表板能區分 queue saturation、KV pressure、GPU failure 與 network/transfer degradation;以及
  10. 重複測試揭露變異,而不是只發布最佳一次。
目錄
  1. 01開始之前:先備知識與學習成果
  2. 02你將建置的成品
  3. 03凍結服務系統
  4. 04凍結工作負載分布
  5. 05閉迴路與開迴路回答不同問題
  6. 閉迴路 Concurrency 測試
  7. 開迴路 Request-Rate 測試
  8. 06在測試前先定義指標
  9. 07控制快取狀態,不要把它平均掉
  10. Cold Cache
  11. Warm Cache
  12. 08完整範例:找出符合 SLO 的容量
  13. 09包含 Headroom 的容量規劃範例
  14. 10建立 Workload Manifest
  15. 11建立 SLO 分析器
  16. 12可觀測性:量測工作、狀態與故障
  17. 請求與 SLO 訊號
  18. Scheduler 與 Queue 訊號
  19. KV 與 GPU 訊號
  20. 分散式訊號
  21. 13刻意破壞的 Benchmark
  22. 14故障注入:證明儀表板有效
  23. 15動手實作
  24. 16可量測的驗收標準
  25. 17提取練習
  26. 18解答
  27. 19本文不涵蓋的內容
  28. 20來源與延伸閱讀

推論基準測試不是比誰印出的 tokens-per-second 最大。它是一個實驗,用來回答:一套版本固定的系統,能否在足夠比例的請求都符合明確延遲與可靠性目標時,持續承受指定工作負載。如果沒有工作負載、抵達過程、快取狀態、分母與百分位數,吞吐量數字就無法回答容量問題。

本篇會把 I-06 的部署配置轉成操作成品。你將凍結混合工作負載,執行飽和與速率型測試,計算請求指標與 goodput(有效吞吐量),推導包含預留空間的副本需求,定義儀表板,並注入一個應該讓服務以安全、可觀測方式失敗的故障。

開始之前:先備知識與學習成果

你應該已理解 TTFT、ITL、TPOT、端對端延遲、連續批次處理、KV 快取,以及 I-06 建立的 TP2 × DP4 部署方式。

完成後,你將能夠:

  1. 區分閉迴路 concurrency 測試與開迴路 request-rate 測試;

  2. 凍結工作負載分布,而不是只測一種方便的 prompt 形狀;

  3. 分開 cold-cache 與 warm-cache 結果;

  4. 計算 TTFT、ITL、TPOT、端對端延遲、吞吐量、觀測 goodput 與符合 SLO 的容量;

  5. 從實測容量與預留比例推導副本及 GPU 數量;

  6. 選擇請求、佇列、KV、GPU、網路與故障指標;以及

  7. 證明警示與復原流程在故障注入下確實有效。

你將建置的成品

你會產出一個版本化的 inference-capacity-pack/,包含四個可檢查檔案:

plaintext
inference-capacity-pack/
├── workload-manifest.yaml
├── requests.csv
├── slo-report.json
└── failure-test.md

workload-manifest.yaml 凍結系統與流量;requests.csv 每一列代表一筆完成或失敗的請求;slo-report.json 包含百分位數、錯誤率、SLO 達成率與 goodput;failure-test.md 記錄一個刻意注入的故障、觸發的警示與復原時間。

凍結服務系統

計算範例使用以下假設性、但規格完整的設定:

輸入 — 假設

--- — ---:

模型 — dense-70b-fixture-r1,I-06 的密集型 BF16 模型

部署 — 4 個副本,每個 TP2,共 8 張 GPU

引擎與 tokenizer — tutorial-engine-fixture-1.0dense-70b-tokenizer-fixture-r1

串流 — 啟用

前綴快取 — 啟用,但 cold 與 warm 測試分開

Speculative decoding — 停用

P/D disaggregation — 停用

量測邊界 — 從 client 送出到 client 收到

主要 SLO 達成目標 — 每個類別都獨立要求至少 95% 成功請求同時符合 request-level TTFT 與 TPOT cutoff

最大錯誤率 — 1%

容量預留 — 正式操作不得超過實測 SLO 合格容量的 75%

引擎與 tokenizer revision 很重要,因為不同版本可能改變指標行為與 token 數。目前 vLLM 文件也警告,不同工具並沒有完全標準化指標術語,因此名稱旁必須附上公式與量測邊界(vLLM Benchmark CLI)。

凍結工作負載分布

單一平均輸入長度會抹去 prefill-heavy 與 decode-heavy 流量的差異。請明確定義類別:

類別 — 比例 — 輸入 tokens — 輸出 tokens — Request TTFT cutoff — Request TPOT cutoff

--- — ---: — ---: — ---: — ---: — ---:

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

對這個固定比例:

plaintext
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

加權值可用來驗證 aggregate counter,但每個類別的 SLO 達成率仍須獨立評估。整體百分位數或達成率可能看起來健康,同時低流量的 long-context 類別卻不健康。

Manifest 還應凍結 sampling 參數、停止行為、chat template、前綴分布、請求取消策略、測試時間、client 位置、完整模型與引擎配置,以及 random seed 或 request schedule。目前 NVIDIA AIPerf 文件支援明確的 constant、Poisson、gamma、fixed-schedule 與 concurrency-burst 抵達模式;選擇哪一種抵達模型是實驗的一部分,不是隱藏的 client 細節(AIPerf load-generator options)。

閉迴路與開迴路回答不同問題

閉迴路 Concurrency 測試

閉迴路測試最多維持 N 筆 in-flight 請求。一筆完成後,client 才送出下一筆。延遲增加時,完成速度下降,新的抵達也隨之變慢。這適合找飽和點與吞吐量—延遲前緣,但它會自我節流,因此無法證明服務能吸收獨立的正式抵達率。

目前 AIPerf 把只有 concurrency 的模式稱為 burst 或 saturation 模式:在 concurrency 上限內盡快送出請求(AIPerf arrival patterns)。

開迴路 Request-Rate 測試

開迴路測試依目標速率安排抵達,不受完成速度影響,常使用 Poisson 過程或實際 trace。如果 offered load 超過服務容量,佇列會成長,TTFT 會把超載暴露出來。這才適合驗證外部抵達率 SLO;但必須確認 load generator 沒有因過低的 concurrency 上限,而在暗中阻塞已排定的抵達。

兩種都要使用:

  1. 以閉迴路 concurrency sweep 找出飽和候選點;

  2. 以開迴路 rate sweep 驗證真實抵達率下的佇列穩定性與 SLO 達成率;以及

  3. 有正式抵達與長度分布時,重播帶 timestamp 的 trace。

在測試前先定義指標

對請求 i,client 送出時間為 s_i、第一個非空 token 時間為 f_i、最後一個 token 時間為 e_i、輸出 token 數為 O_i

plaintext
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,個別 inter-token latency 為:

plaintext
ITL_i,j = t_i,j - t_i,j-1, for j = 2 ... O_i

每筆請求只有一個 TPOT 平均值;ITL 則是所有個別間隔的分布。一般一次一 token 的串流下兩者相近;但一個串流 chunk 內含多個 token 時,包括 speculative decoding,兩者就可能分歧。目前 vLLM 基準測試文件明確區分這兩者(vLLM Benchmark CLI)。

對長度為 T 的量測視窗:

plaintext
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

本篇將 capacity goodput 定義為同時符合以下條件的最高 offered request rate:

  • 每個工作負載類別都獨立達到至少 95% request-level SLO 達成率;

  • 每個類別都有至少 2,000 筆成功觀測;

  • 錯誤率不超過 1%;以及

  • steady-state 視窗中沒有持續佇列成長。

整體 observed goodput 仍是有用的吞吐量計數器,但單一類別失敗時,它不能讓容量測試點通過。逐類別閘門可防止占 70% 的 interactive 類別掩蓋占 20% 或 10% 的類別退化。

DistServe 使用 goodput 把抵達率與同時成立的 TTFT、TPOT 限制連在一起,而不是只計算原始吞吐量;它報告的改善仍只適用於其評估的模型、工作負載和 SLO(Zhong et al., OSDI 2024)。

控制快取狀態,不要把它平均掉

執行兩組有明確名稱的實驗:

Cold Cache

  • 每個測試點前重啟或明確清除 engine cache;

  • 無法清除時使用不重複前綴;

  • steady-state 視窗排除 compilation 與 model-load warmup;以及

  • 回報接近零的 cache-hit rate。

Warm Cache

  • 預熱一組版本固定的重複前綴;

  • 不同配置使用相同的前綴頻率分布;

  • 只有在預期快取狀態成立後才開始量測;以及

  • 回報 prefix-cache hit rate 與 eviction rate。

不要把 cold 與 warm 請求混成一個未標記平均值。目前 vLLM 基準測試指南警告,對同一伺服器重跑可重現資料集,可能重用快取內殘留的前綴並膨脹吞吐量(vLLM Benchmark CLI)。

完整範例:找出符合 SLO 的容量

假設每個測試點使用固定 seed 的 Poisson 抵達排程,預先分層為恰好 15,400 筆 interactive、4,400 筆 long-context 與 2,200 筆 generation-heavy 請求,再進行隨機排列,共 22,000 筆。至少量測五分鐘;若任何類別完成後少於 2,000 筆成功觀測,便沿用預先承諾的排程繼續執行。快取策略保持不變。下列是參數化教學資料,不是 OptiVerse 基礎設施的量測:

Offered RPS — Completed RPS — 錯誤率 — 整體達成率 — 最低類別達成率 — 最低類別成功 n — 觀測 goodput RPS — Output 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 — 20.0 — 0.1% — 99.1% — 98.2% — 2,197 — 19.82 — 4,864 — 260 ms — 29 ms — 平坦

24 — 23.9 — 0.4% — 95.4% — 95.1% — 2,191 — 22.80 — 5,812 — 420 ms — 38 ms — 平坦

28 — 25.2 — 2.0% — 86.8% — 78.0% — 2,156 — 21.87 — 6,129 — 980 ms — 62 ms — 成長

28 RPS 測試點的原始 output throughput 最高,但觀測 goodput 低於 24 RPS。它最弱類別的達成率只有 78.0%,也同時違反兩個 interactive 延遲目標、錯誤率過高,而且佇列正在成長。只看原始吞吐量會選錯操作點。

每個類別都至少有 95% 達成率與 2,000 筆成功觀測、整體錯誤率不超過 1%,且佇列保持平坦的最高 offered rate,是四副本下的 24 RPS。其最弱類別在 2,191 筆成功請求上達到 95.1%。因此:

plaintext
measured SLO-compliant capacity per replica
  = 24 RPS / 4 replicas
  = 6 RPS/replica

只有在工作負載比例、部署、快取狀態、引擎配置與 scaling regime 都相同時,才能做這個除法。它不是通用的每副本常數。

包含 Headroom 的容量規劃範例

假設預測尖峰為 36 RPS,政策預留 25% 容量,因此規劃利用率最多 75%:

plaintext
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 叢集只有四個副本,無法在指定預留比例下承受預測尖峰。這個計算本身不能授權採購:仍須驗證八副本拓撲,因為 load balancer 行為、網路路徑、快取 locality 與共享瓶頸,都可能阻止線性 scaling。目前 Dynamo sizing 文件也明確劃出相同邊界:分析型 optimizer 會提出配置,但不會模擬逐請求 scheduler 或 KV-cache 行為,因此仍必須執行 live benchmark(Dynamo sizing with AIConfigurator)。

Little's Law 可用於檢查穩定視窗的一致性:

plaintext
average_in_flight ≈ completion_rate × average_E2E_seconds

在 23.9 completed RPS、平均 E2E 1.8 秒時,預期平均 in-flight 數為 23.9 × 1.8 = 43.02。Client/server in-flight gauge 若相差很大,代表量測邊界不一致、遙測遺失、重試,或佇列不穩定。Little's Law 無法挽救持續成長的超載佇列,因為穩定系統假設已不成立。

建立 Workload Manifest

建立 inference-capacity-pack/workload-manifest.yaml

yaml
cutoff: 2026-08-29
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: 20260829
  class_schedule_counts: {interactive: 15400, long_context: 4400, generation_heavy: 2200}
  warmup_seconds: 60
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.75

request_*_cutoff_ms 欄位套用到個別請求;P50/P95/P99 仍是要回報的分布統計,不能和這些 cutoff 互換。先產生精確類別筆數,以記錄的 seed 洗牌,再配置 Poisson 抵達間隔。初始排程結束後,若任何類別少於 2,000 筆成功列,必須從同一個版本化排程接續,再做容量決策。

如果使用 AIPerf,可用 --request-rate、選定的 arrival pattern、streaming、duration 與凍結資料集表示 rate sweep。只有 concurrency 的 sweep 應該另命名為 saturation experiment。目前 AIPerf 文件同時提供這兩種控制,並會寫出後續分析所需的成品(AIPerf command-line options)。

建立 SLO 分析器

把 load tool 的 per-request 輸出正規化為 requests.csv,包含以下欄位:

plaintext
request_id,class,ttft_ms,tpot_ms,e2e_ms,output_tokens,status
r-0001,interactive,182.0,25.1,3370.0,128,ok
r-0002,long_context,1940.0,44.2,13211.2,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

python
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 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))

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"])
    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))

執行:

bash
python analyze_slo.py inference-capacity-pack/requests.csv 300 flat \
  > inference-capacity-pack/slo-report.json

上方四列 CSV 是 schema smoke test,因此每個類別的樣本刻意不足,capacity_gate_pass 必須為 false。在真實測試點,只有每個命名類別都有至少 2,000 筆成功列、joint request-level 達成率至少 95%、錯誤率不超過 1%,且佇列趨勢為 flat 時,分析器才會讓容量通過。只有 scheduler queue-depth time series 顯示沒有持續成長時,才能傳入 flat。這個 script 使用 nearest-rank percentile,確保結果可重現。正式工具可能採用不同 interpolation;記錄演算法,不要盲目比較 percentile 名稱。

可觀測性:量測工作、狀態與故障

正式儀表板需要足夠訊號,才能解釋 SLO 為什麼改變。請使用 model revision、engine version、workload class、replica 和 result 等有界 cardinality 維度。Request ID 應放在 trace/log,不要當成 metric label。

請求與 SLO 訊號

  • attempted、successful、failed、timed-out、rejected 與 cancelled requests;

  • 依 workload class 區分的 TTFT、TPOT、ITL 與 E2E histogram;

  • input/output tokens 與 request/output throughput;

  • observed goodput 與逐類別 SLO attainment;以及

  • retry count 與 client disconnects。

Scheduler 與 Queue 訊號

  • waiting 與 running requests;

  • queue-wait histogram;

  • batched prompt 與 generation tokens;

  • preemption、eviction 與 admission rejection counts;以及

  • 引擎支援時的 per-iteration batch composition。

KV 與 GPU 訊號

  • KV blocks used/total 與 maximum token capacity;

  • prefix-cache hit 與 eviction rates;

  • HBM used、compute utilization、memory bandwidth、power 與 OOM events;以及

  • model load 與 warmup duration。

分散式訊號

  • 依 operation/message bucket 區分的 collective duration 與 errors;

  • disaggregated serving 的 KV-transfer bytes、latency、throughput 與 failures;

  • per-replica inflight work 與 router selection;以及

  • 啟用 speculative decoding 時的 speculative acceptance length/rate。

目前 NVIDIA Dynamo 文件會比較 vLLM、SGLang 與 TensorRT-LLM 提供的 Prometheus 訊號,並警告 metric 名稱會隨引擎版本改變;應檢查實際部署的 /metrics endpoint,而不是直接沿用舊儀表板(Dynamo Engine Metrics Comparison)。

刻意破壞的 Benchmark

這個配置能產出亮眼數字,卻幾乎沒有操作證據:

yaml
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

它壞在:

  1. concurrency-only 流量會自我節流,無法驗證獨立抵達率;

  2. 100 筆請求幾乎沒有 tail evidence;

  3. 重複 prompt 與未清除快取會混合 cold/warm 狀態;

  4. 平均值會隱藏 tail latency 與類別失敗;

  5. 沒有錯誤、佇列成長、取消與快取狀態;以及

  6. 可變的 latest 版本無法重現。

修復方式不是再加一個 percentile,而是換成版本固定的 manifest、分離快取狀態、兩種負載模式、per-request 記錄、類別 SLO 與 failure test。

故障注入:證明儀表板有效

讓服務在通過條件的 20 RPS 測試點運行,接著終止四個 TP2 副本中的一個。在 failure-test.md 記錄:

plaintext
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% 副本理應降低容量。系統只有在能偵測並移除副本、不再把新工作送給它、誠實暴露錯誤或 backpressure,並回到穩定狀態時才通過。若剩餘三個副本無法在 SLO 內承受 20 RPS,admission control 應拒絕或 shed load,而不是讓佇列無限成長。

之後再分別測試冷啟動 autoscaled replica、降低 KV 容量、request cancellation 與退化的跨節點連線。這些測試針對不同故障機制,不應壓成一個 chaos score。

動手實作

  1. 產生或擷取符合三類 manifest 的工作負載。

  2. 執行閉迴路 concurrency sweep 找出飽和點。

  3. 對 16、20、24、28 RPS 使用固定 seed 的 22,000-request 排程,分別執行至少五分鐘 cold-cache 與 warm-cache Poisson rate sweep。

  4. 把每筆 attempted request 正規化到 requests.csv;繼續執行排程,直到每個類別都有至少 2,000 筆成功列。

  5. 產生 slo-report.json,並畫出 offered RPS 對 raw throughput、observed goodput、逐類別 attainment、P95 TTFT、P95 TPOT 的圖。

  6. 選擇分析器回報 capacity_gate_pass: true 的最高速率。

  7. 以 75% target utilization 計算 36 RPS 所需副本與 GPU。

  8. 在通過條件的測試點注入單副本故障,完成 failure-test.md

若某類別少於 2,000 筆成功樣本,請把 tail 結果標成探索性,而且不得把該測試點選為容量。分層排程保證占 10% 的類別一開始有 2,200 次嘗試;錯誤發生後,分析器仍會檢查成功筆數。某類別少於 10,000 筆成功觀測時,不要報告精確 P99;否則最高 1% 大約只剩不到 100 筆觀測。

可量測的驗收標準

Capacity pack 只有在以下條件成立時才通過:

  • model、tokenizer、engine、precision、scheduler 與 workload revision 都不可變;

  • cold/warm cache 測試分別命名,並回報 hit/eviction 狀態;

  • 同時包含閉迴路 saturation 與開迴路 arrival-rate sweep;

  • 每個測試點都回報 attempted/completed/failed requests、error rate、P50/P95 latency、throughput、goodput、queue trend 與逐類別成功筆數;

  • 每個類別都分別依 request-level TTFT/TPOT cutoff 評估,且至少有 2,000 筆成功觀測;

  • 選定容量點的每個類別都至少有 95% joint request-level 達成率、最多 1% 錯誤,且沒有持續 queue growth;

  • 副本計算包含 75% utilization 政策,並對範例輸入得到 8 replicas/16 GPUs;

  • failure test 記錄 detection/recovery time,而且至少一個已配置 alert 觸發;

  • 儀表板能區分 queue saturation、KV pressure、GPU failure 與 network/transfer degradation;以及

  • 重複測試揭露變異,而不是只發布最佳一次。

提取練習

  1. 為什麼閉迴路測試可能隱藏超載?

  2. TPOT 與 ITL 有什麼不同?

  3. 為什麼 cold-cache 與 warm-cache 結果必須分開?

  4. Observed goodput 與 raw request throughput 有何差異?

  5. 在計算範例中,為什麼選 24 offered RPS,而不是 28?

  6. 在指定預留比例下,36 RPS 需要多少副本與 GPU?

  7. 哪些訊號能區分 queue problem 與 KV-memory pressure?

  8. Failure test 能證明什麼健康 benchmark 無法證明的事?

解答

  1. 新工作只在先前工作完成後才送出,因此延遲升高會減慢抵達,讓 client 自我節流。

  2. TPOT 是每筆請求的一個平均 decode 時間;ITL 包含串流輸出之間的每一個實際間隔。

  3. 前綴重用會降低 prefill 工作並膨脹吞吐量;混合兩種狀態會讓結果不可重現,也無法解釋工作負載。

  4. 它每單位時間只計算符合所屬類別 request-level cutoff 的成功請求;容量還要求每個類別都通過自己的達成率與樣本數閘門。

  5. 28 RPS 的 raw throughput 較高,但最弱類別達成率只有 78.0%、錯誤率是 2.0%,而且佇列成長。24 RPS 的每個類別都在至少 2,191 筆成功請求上達到至少 95.1%,其他閘門也都通過。

  6. ceil(36/(6×0.75)) = 8 個副本;每個 TP2,因此共 16 張 GPU。

  7. Queue depth/wait 與 admission metrics 顯示飽和;KV utilization、free blocks、eviction 與 preemption 顯示 KV pressure。GPU 與 request metrics 提供交叉證據。

  8. 它證明在已知故障下,偵測、路由移除、load shedding/retry、警示與復原流程都能運作。

本文不涵蓋的內容

本篇定義的是推論操作方法,不是通用 SLO。產品團隊必須依使用者需求與成本/風險限制推導目標。參數化結果不是 OptiVerse 量測,線性副本計算也必須在 scale-out 後驗證。即使基礎設施 SLO 通過,品質評估、模型準確度、安全與應用正確性仍是獨立 gate。


來源與延伸閱讀

  • vLLM Project. “Benchmark CLI.” 現行文件,存取日期 2026-08-29。Client-observed TTFT、ITL、TPOT、throughput 與 cache-warmth 警告的第一方定義。docs.vllm.ai

  • NVIDIA. “AIPerf Load Generator Options Reference.” 現行文件,存取日期 2026-08-29。Rate、concurrency、trace 與 user-driven load schedule 的第一方定義。docs.nvidia.com/aiperf

  • NVIDIA. “AIPerf Command Line Options.” 現行文件,存取日期 2026-08-29。Request-rate sweep、Poisson/gamma/constant arrivals、concurrency、warmup、duration 與 cancellation 的第一方控制說明。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 serving 中 SLO-conditioned goodput 的第一手來源。usenix.org

  • NVIDIA Dynamo. “Engine Metrics Comparison.” 現行文件,存取日期 2026-08-29。Request、queue、KV、routing、transfer 與 speculation metric 的第一方跨引擎清單。docs.nvidia.com/dynamo

  • NVIDIA Dynamo. “Size a Local Deployment with AIConfigurator.” 現行文件,存取日期 2026-08-29。估算候選配置後,再對 live endpoint 驗證的第一方指南。docs.nvidia.com/dynamo

  • NVIDIA. “LLM Inference Benchmarking: Fundamental Concepts.” 2025 年 4 月。Metric 定義、workload length 與 throughput-latency trade-off 的第一方說明。developer.nvidia.com

閱讀順序

推論系統

章節 08 / 08

你在這裡100%
  1. 01
  2. 02
  3. 03
  4. 04
  5. 05
  6. 06
  7. 07
  8. 08

系列完成!

恭喜!你已完成這個系列。以下是推薦的下一步:

分享這篇文章
XLinkedIn

Huang Tzu Lin

With over eight years in autonomous robotics, there's a strong passion for incorporating cutting-edge technologies and innovative approaches. Dedicated to transforming the latest research and insights into practical applications, this journey pushes the limits of possibility.

透過 RSS 訂閱

使用慣用的 RSS 閱讀器追蹤最新的 AI 系統實作教學。

開啟 RSS Feed

支援任何 RSS 閱讀器,新文章將自動送達。

回到頂端