94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
674 lines
22 KiB
Python
674 lines
22 KiB
Python
# Adapted from benchmark/hicache/bench_serving.py and python/sglang/bench_serving.py
|
|
|
|
"""
|
|
Benchmark warm-cache serving with exact shared-prefix control.
|
|
|
|
This benchmark is designed for cache-focused studies where each request has a
|
|
fixed total input length and an exactly controlled shared-prefix ratio. For each
|
|
shared-prefix percentage, the benchmark:
|
|
|
|
1. Flushes the server KV cache.
|
|
2. Builds prompts with an identical shared prefix and random unique suffixes.
|
|
3. Warms only the shared prefix once.
|
|
4. Benchmarks the full prompts through SGLang's native /generate endpoint.
|
|
|
|
Compared with the existing hicache shared-prefix benchmarks, this benchmark
|
|
provides direct control over total length, shared-prefix length, and suffix
|
|
length at the token-id level.
|
|
"""
|
|
|
|
import argparse
|
|
import asyncio
|
|
import json
|
|
import random
|
|
import time
|
|
import warnings
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
import aiohttp
|
|
import numpy as np
|
|
import requests
|
|
from transformers import PreTrainedTokenizerBase
|
|
|
|
from sglang.benchmark.utils import get_tokenizer, remove_prefix, set_ulimit
|
|
|
|
AIOHTTP_TIMEOUT = aiohttp.ClientTimeout(total=20 * 60 * 60)
|
|
AIOHTTP_READ_BUFSIZE = 10 * 1024**2
|
|
|
|
|
|
global args
|
|
|
|
|
|
@dataclass
|
|
class RequestFuncOutput:
|
|
generated_text: str = ""
|
|
success: bool = False
|
|
latency: float = 0.0
|
|
ttft: float = 0.0
|
|
itl: List[float] = field(default_factory=list)
|
|
prompt_len: int = 0
|
|
error: str = ""
|
|
output_len: int = 0
|
|
start_time: float = 0.0
|
|
|
|
|
|
@dataclass
|
|
class BenchmarkMetrics:
|
|
completed: int
|
|
total_input: int
|
|
total_output: int
|
|
total_output_retokenized: int
|
|
request_throughput: float
|
|
input_throughput: float
|
|
output_throughput: float
|
|
output_throughput_retokenized: float
|
|
total_throughput: float
|
|
total_throughput_retokenized: float
|
|
mean_ttft_ms: float
|
|
median_ttft_ms: float
|
|
std_ttft_ms: float
|
|
p90_ttft_ms: float
|
|
p99_ttft_ms: float
|
|
mean_tpot_ms: float
|
|
median_tpot_ms: float
|
|
std_tpot_ms: float
|
|
p90_tpot_ms: float
|
|
p99_tpot_ms: float
|
|
mean_itl_ms: float
|
|
median_itl_ms: float
|
|
std_itl_ms: float
|
|
p90_itl_ms: float
|
|
p99_itl_ms: float
|
|
mean_e2e_latency_ms: float
|
|
median_e2e_latency_ms: float
|
|
std_e2e_latency_ms: float
|
|
p99_e2e_latency_ms: float
|
|
concurrency: float
|
|
|
|
|
|
def _create_bench_client_session() -> aiohttp.ClientSession:
|
|
return aiohttp.ClientSession(
|
|
timeout=AIOHTTP_TIMEOUT,
|
|
read_bufsize=AIOHTTP_READ_BUFSIZE,
|
|
)
|
|
|
|
|
|
async def async_request_sglang_generate(
|
|
api_url: str,
|
|
input_ids: List[int],
|
|
prompt_len: int,
|
|
output_len: int,
|
|
pbar: Optional[Any] = None,
|
|
) -> RequestFuncOutput:
|
|
async with _create_bench_client_session() as session:
|
|
payload = {
|
|
"input_ids": input_ids,
|
|
"sampling_params": {
|
|
"temperature": 0.0,
|
|
"max_new_tokens": output_len,
|
|
"ignore_eos": not args.disable_ignore_eos,
|
|
},
|
|
"stream": True,
|
|
**args.extra_request_body,
|
|
}
|
|
|
|
output = RequestFuncOutput(prompt_len=prompt_len)
|
|
|
|
generated_text = ""
|
|
ttft = 0.0
|
|
st = time.perf_counter()
|
|
output.start_time = st
|
|
most_recent_timestamp = st
|
|
last_output_len = 0
|
|
latency = 0.0
|
|
|
|
try:
|
|
async with session.post(url=api_url, json=payload) as response:
|
|
if response.status == 200:
|
|
async for chunk_bytes in response.content:
|
|
chunk_bytes = chunk_bytes.strip()
|
|
if not chunk_bytes:
|
|
continue
|
|
|
|
chunk = remove_prefix(chunk_bytes.decode("utf-8"), "data: ")
|
|
latency = time.perf_counter() - st
|
|
if chunk == "[DONE]":
|
|
continue
|
|
|
|
data = json.loads(chunk)
|
|
|
|
if "text" in data and data["text"]:
|
|
timestamp = time.perf_counter()
|
|
generated_text = data["text"]
|
|
current_output_len = data["meta_info"]["completion_tokens"]
|
|
|
|
if ttft == 0.0:
|
|
ttft = timestamp - st
|
|
output.ttft = ttft
|
|
else:
|
|
num_new_tokens = current_output_len - last_output_len
|
|
if num_new_tokens == 0:
|
|
continue
|
|
chunk_gap = timestamp - most_recent_timestamp
|
|
adjust_itl = chunk_gap / num_new_tokens
|
|
output.itl.extend([adjust_itl] * num_new_tokens)
|
|
|
|
most_recent_timestamp = timestamp
|
|
last_output_len = current_output_len
|
|
output.output_len = current_output_len
|
|
|
|
output.generated_text = generated_text
|
|
output.success = True
|
|
output.latency = latency
|
|
else:
|
|
output.error = (
|
|
(response.reason or "") + ": " + (await response.text())
|
|
)
|
|
output.success = False
|
|
except Exception as exc:
|
|
output.success = False
|
|
output.error = str(exc)
|
|
|
|
if pbar:
|
|
pbar.update(1)
|
|
return output
|
|
|
|
|
|
async def run_batch(
|
|
api_url: str,
|
|
prompts: List[Dict[str, Any]],
|
|
output_len: int,
|
|
max_concurrency: Optional[int],
|
|
pbar: Optional[Any] = None,
|
|
) -> List[RequestFuncOutput]:
|
|
semaphore = asyncio.Semaphore(max_concurrency) if max_concurrency else None
|
|
|
|
async def limited_request(prompt: Dict[str, Any]) -> RequestFuncOutput:
|
|
if semaphore is None:
|
|
return await async_request_sglang_generate(
|
|
api_url=api_url,
|
|
input_ids=prompt["input_ids"],
|
|
prompt_len=prompt["prompt_len"],
|
|
output_len=output_len,
|
|
pbar=pbar,
|
|
)
|
|
async with semaphore:
|
|
return await async_request_sglang_generate(
|
|
api_url=api_url,
|
|
input_ids=prompt["input_ids"],
|
|
prompt_len=prompt["prompt_len"],
|
|
output_len=output_len,
|
|
pbar=pbar,
|
|
)
|
|
|
|
tasks = [asyncio.create_task(limited_request(prompt)) for prompt in prompts]
|
|
return await asyncio.gather(*tasks)
|
|
|
|
|
|
def flush_cache(base_url: str) -> None:
|
|
response = requests.post(f"{base_url}/flush_cache", timeout=30)
|
|
response.raise_for_status()
|
|
|
|
|
|
def gen_token_ids(
|
|
vocab_ids: List[int],
|
|
token_num: int,
|
|
rng: random.Random,
|
|
) -> List[int]:
|
|
if token_num <= 0:
|
|
return []
|
|
return rng.choices(vocab_ids, k=token_num)
|
|
|
|
|
|
def build_prompts(
|
|
vocab_ids: List[int],
|
|
total_tokens: int,
|
|
shared_pct: int,
|
|
num_prompts: int,
|
|
rng: random.Random,
|
|
) -> List[Dict[str, Any]]:
|
|
prefix_len = total_tokens * shared_pct // 100
|
|
suffix_len = total_tokens - prefix_len
|
|
|
|
shared_prefix = gen_token_ids(vocab_ids, prefix_len, rng)
|
|
|
|
prompts: List[Dict[str, Any]] = []
|
|
for _ in range(num_prompts):
|
|
suffix = gen_token_ids(vocab_ids, suffix_len, rng)
|
|
input_ids = shared_prefix + suffix
|
|
prompts.append({"input_ids": input_ids, "prompt_len": len(input_ids)})
|
|
|
|
return prompts
|
|
|
|
|
|
async def warm_shared_prefix(api_url: str, shared_prefix_ids: List[int]) -> None:
|
|
if not shared_prefix_ids:
|
|
return
|
|
|
|
warmup = await async_request_sglang_generate(
|
|
api_url=api_url,
|
|
input_ids=shared_prefix_ids,
|
|
prompt_len=len(shared_prefix_ids),
|
|
output_len=1,
|
|
)
|
|
if not warmup.success:
|
|
raise RuntimeError(
|
|
"Warmup failed - Please make sure benchmark arguments are correctly "
|
|
f"specified. Error: {warmup.error}"
|
|
)
|
|
|
|
|
|
def calculate_metrics(
|
|
outputs: List[RequestFuncOutput],
|
|
dur_s: float,
|
|
tokenizer: PreTrainedTokenizerBase,
|
|
) -> Tuple[BenchmarkMetrics, List[int]]:
|
|
output_lens: List[int] = []
|
|
retokenized_output_lens: List[int] = []
|
|
total_input = 0
|
|
completed = 0
|
|
itls: List[float] = []
|
|
tpots: List[float] = []
|
|
ttfts: List[float] = []
|
|
e2e_latencies: List[float] = []
|
|
|
|
for output in outputs:
|
|
if output.success:
|
|
output_len = output.output_len
|
|
output_lens.append(output_len)
|
|
retokenized_output_len = len(
|
|
tokenizer.encode(output.generated_text, add_special_tokens=False)
|
|
)
|
|
retokenized_output_lens.append(retokenized_output_len)
|
|
total_input += output.prompt_len
|
|
if output_len > 1:
|
|
tpots.append((output.latency - output.ttft) / (output_len - 1))
|
|
itls += output.itl
|
|
ttfts.append(output.ttft)
|
|
e2e_latencies.append(output.latency)
|
|
completed += 1
|
|
else:
|
|
output_lens.append(0)
|
|
retokenized_output_lens.append(0)
|
|
|
|
if completed == 0:
|
|
warnings.warn(
|
|
"All requests failed. This is likely due to a misconfiguration "
|
|
"on the benchmark arguments.",
|
|
stacklevel=2,
|
|
)
|
|
|
|
metrics = BenchmarkMetrics(
|
|
completed=completed,
|
|
total_input=total_input,
|
|
total_output=sum(output_lens),
|
|
total_output_retokenized=sum(retokenized_output_lens),
|
|
request_throughput=completed / dur_s,
|
|
input_throughput=total_input / dur_s,
|
|
output_throughput=sum(output_lens) / dur_s,
|
|
output_throughput_retokenized=sum(retokenized_output_lens) / dur_s,
|
|
total_throughput=(total_input + sum(output_lens)) / dur_s,
|
|
total_throughput_retokenized=(total_input + sum(retokenized_output_lens))
|
|
/ dur_s,
|
|
mean_ttft_ms=np.mean(ttfts or 0) * 1000,
|
|
median_ttft_ms=np.median(ttfts or 0) * 1000,
|
|
std_ttft_ms=np.std(ttfts or 0) * 1000,
|
|
p90_ttft_ms=np.percentile(ttfts or 0, 90) * 1000,
|
|
p99_ttft_ms=np.percentile(ttfts or 0, 99) * 1000,
|
|
mean_tpot_ms=np.mean(tpots or 0) * 1000,
|
|
median_tpot_ms=np.median(tpots or 0) * 1000,
|
|
std_tpot_ms=np.std(tpots or 0) * 1000,
|
|
p90_tpot_ms=np.percentile(tpots or 0, 90) * 1000,
|
|
p99_tpot_ms=np.percentile(tpots or 0, 99) * 1000,
|
|
mean_itl_ms=np.mean(itls or 0) * 1000,
|
|
median_itl_ms=np.median(itls or 0) * 1000,
|
|
std_itl_ms=np.std(itls or 0) * 1000,
|
|
p90_itl_ms=np.percentile(itls or 0, 90) * 1000,
|
|
p99_itl_ms=np.percentile(itls or 0, 99) * 1000,
|
|
mean_e2e_latency_ms=np.mean(e2e_latencies) * 1000,
|
|
median_e2e_latency_ms=np.median(e2e_latencies) * 1000,
|
|
std_e2e_latency_ms=np.std(e2e_latencies) * 1000,
|
|
p99_e2e_latency_ms=np.percentile(e2e_latencies, 99) * 1000,
|
|
concurrency=np.sum(e2e_latencies) / dur_s,
|
|
)
|
|
return metrics, output_lens
|
|
|
|
|
|
def print_benchmark_result(
|
|
metrics: BenchmarkMetrics,
|
|
benchmark_duration: float,
|
|
backend: str,
|
|
request_rate: float,
|
|
max_concurrency: Optional[int],
|
|
) -> None:
|
|
print("\n{s:{c}^{n}}".format(s=" Serving Benchmark Result ", n=50, c="="))
|
|
print("{:<40} {:<10}".format("Backend:", backend))
|
|
print("{:<40} {:<10}".format("Traffic request rate:", request_rate))
|
|
print(
|
|
"{:<40} {:<10}".format(
|
|
"Max request concurrency:",
|
|
max_concurrency if max_concurrency else "not set",
|
|
)
|
|
)
|
|
print("{:<40} {:<10}".format("Successful requests:", metrics.completed))
|
|
print("{:<40} {:<10.2f}".format("Benchmark duration (s):", benchmark_duration))
|
|
print("{:<40} {:<10}".format("Total input tokens:", metrics.total_input))
|
|
print("{:<40} {:<10}".format("Total generated tokens:", metrics.total_output))
|
|
print(
|
|
"{:<40} {:<10}".format(
|
|
"Total generated tokens (retokenized):", metrics.total_output_retokenized
|
|
)
|
|
)
|
|
print(
|
|
"{:<40} {:<10.2f}".format(
|
|
"Request throughput (req/s):", metrics.request_throughput
|
|
)
|
|
)
|
|
print(
|
|
"{:<40} {:<10.2f}".format(
|
|
"Input token throughput (tok/s):", metrics.input_throughput
|
|
)
|
|
)
|
|
print(
|
|
"{:<40} {:<10.2f}".format(
|
|
"Output token throughput (tok/s):", metrics.output_throughput
|
|
)
|
|
)
|
|
print(
|
|
"{:<40} {:<10.2f}".format(
|
|
"Total token throughput (tok/s):", metrics.total_throughput
|
|
)
|
|
)
|
|
print("{:<40} {:<10.2f}".format("Concurrency:", metrics.concurrency))
|
|
print("{s:{c}^{n}}".format(s="End-to-End Latency", n=50, c="-"))
|
|
print(
|
|
"{:<40} {:<10.2f}".format("Mean E2E Latency (ms):", metrics.mean_e2e_latency_ms)
|
|
)
|
|
print(
|
|
"{:<40} {:<10.2f}".format(
|
|
"Median E2E Latency (ms):", metrics.median_e2e_latency_ms
|
|
)
|
|
)
|
|
print("{s:{c}^{n}}".format(s="Time to First Token", n=50, c="-"))
|
|
print("{:<40} {:<10.2f}".format("Mean TTFT (ms):", metrics.mean_ttft_ms))
|
|
print("{:<40} {:<10.2f}".format("Median TTFT (ms):", metrics.median_ttft_ms))
|
|
print("{:<40} {:<10.2f}".format("P90 TTFT (ms):", metrics.p90_ttft_ms))
|
|
print("{:<40} {:<10.2f}".format("P99 TTFT (ms):", metrics.p99_ttft_ms))
|
|
print(
|
|
"{s:{c}^{n}}".format(s="Time per Output Token (excl. 1st token)", n=50, c="-")
|
|
)
|
|
print("{:<40} {:<10.2f}".format("Mean TPOT (ms):", metrics.mean_tpot_ms))
|
|
print("{:<40} {:<10.2f}".format("Median TPOT (ms):", metrics.median_tpot_ms))
|
|
print("{:<40} {:<10.2f}".format("P90 TPOT (ms):", metrics.p90_tpot_ms))
|
|
print("{:<40} {:<10.2f}".format("P99 TPOT (ms):", metrics.p99_tpot_ms))
|
|
print("{s:{c}^{n}}".format(s="Inter-token Latency", n=50, c="-"))
|
|
print("{:<40} {:<10.2f}".format("Mean ITL (ms):", metrics.mean_itl_ms))
|
|
print("{:<40} {:<10.2f}".format("Median ITL (ms):", metrics.median_itl_ms))
|
|
print("{:<40} {:<10.2f}".format("P90 ITL (ms):", metrics.p90_itl_ms))
|
|
print("{:<40} {:<10.2f}".format("P99 ITL (ms):", metrics.p99_itl_ms))
|
|
print("=" * 50)
|
|
|
|
|
|
def maybe_write_summary_jsonl(
|
|
pct: int,
|
|
prefix_len: int,
|
|
suffix_len: int,
|
|
metrics: BenchmarkMetrics,
|
|
output_file: Optional[str],
|
|
benchmark_duration: float,
|
|
) -> None:
|
|
if not output_file:
|
|
return
|
|
|
|
result = {
|
|
"backend": args.backend,
|
|
"dataset_name": "warm-cache",
|
|
"request_rate": float("inf"),
|
|
"max_concurrency": args.max_concurrency,
|
|
"shared_prefix_pct": pct,
|
|
"prefix_len": prefix_len,
|
|
"suffix_len": suffix_len,
|
|
"total_tokens": args.total_tokens,
|
|
"num_prompts": args.num_prompts,
|
|
"output_len": args.output_len,
|
|
"completed": metrics.completed,
|
|
"benchmark_duration": benchmark_duration,
|
|
"total_input": metrics.total_input,
|
|
"total_output": metrics.total_output,
|
|
"total_output_retokenized": metrics.total_output_retokenized,
|
|
"request_throughput": metrics.request_throughput,
|
|
"input_throughput": metrics.input_throughput,
|
|
"output_throughput": metrics.output_throughput,
|
|
"output_throughput_retokenized": metrics.output_throughput_retokenized,
|
|
"total_throughput": metrics.total_throughput,
|
|
"total_throughput_retokenized": metrics.total_throughput_retokenized,
|
|
"mean_ttft_ms": metrics.mean_ttft_ms,
|
|
"median_ttft_ms": metrics.median_ttft_ms,
|
|
"std_ttft_ms": metrics.std_ttft_ms,
|
|
"p90_ttft_ms": metrics.p90_ttft_ms,
|
|
"p99_ttft_ms": metrics.p99_ttft_ms,
|
|
"mean_tpot_ms": metrics.mean_tpot_ms,
|
|
"median_tpot_ms": metrics.median_tpot_ms,
|
|
"std_tpot_ms": metrics.std_tpot_ms,
|
|
"p90_tpot_ms": metrics.p90_tpot_ms,
|
|
"p99_tpot_ms": metrics.p99_tpot_ms,
|
|
"mean_itl_ms": metrics.mean_itl_ms,
|
|
"median_itl_ms": metrics.median_itl_ms,
|
|
"std_itl_ms": metrics.std_itl_ms,
|
|
"p90_itl_ms": metrics.p90_itl_ms,
|
|
"p99_itl_ms": metrics.p99_itl_ms,
|
|
"mean_e2e_latency_ms": metrics.mean_e2e_latency_ms,
|
|
"median_e2e_latency_ms": metrics.median_e2e_latency_ms,
|
|
"std_e2e_latency_ms": metrics.std_e2e_latency_ms,
|
|
"p99_e2e_latency_ms": metrics.p99_e2e_latency_ms,
|
|
"concurrency": metrics.concurrency,
|
|
}
|
|
|
|
with open(output_file, "a", encoding="utf-8") as fout:
|
|
fout.write(json.dumps(result) + "\n")
|
|
|
|
|
|
async def benchmark_shared_prefix_pct(
|
|
api_url: str,
|
|
base_url: str,
|
|
tokenizer: PreTrainedTokenizerBase,
|
|
vocab_ids: List[int],
|
|
rng: random.Random,
|
|
pct: int,
|
|
) -> Tuple[BenchmarkMetrics, float, int, int, int]:
|
|
prefix_len = args.total_tokens * pct // 100
|
|
suffix_len = args.total_tokens - prefix_len
|
|
|
|
print(f"\n{'=' * 70}")
|
|
print(
|
|
f"shared_prefix={pct}% prefix_len={prefix_len} "
|
|
f"suffix_len={suffix_len} total={prefix_len + suffix_len}"
|
|
)
|
|
print(f"{'=' * 70}")
|
|
|
|
print("Flushing KV cache ...")
|
|
flush_cache(base_url)
|
|
time.sleep(1)
|
|
|
|
print(f"Building {args.num_prompts} prompts ...")
|
|
prompts = build_prompts(
|
|
vocab_ids=vocab_ids,
|
|
total_tokens=args.total_tokens,
|
|
shared_pct=pct,
|
|
num_prompts=args.num_prompts,
|
|
rng=rng,
|
|
)
|
|
|
|
if prefix_len > 0:
|
|
print(f"Warming shared prefix only ({prefix_len} tokens) ...")
|
|
await warm_shared_prefix(
|
|
api_url=api_url, shared_prefix_ids=prompts[0]["input_ids"][:prefix_len]
|
|
)
|
|
|
|
print(f"Sending requests (max_concurrency={args.max_concurrency}) ...")
|
|
benchmark_start_time = time.perf_counter()
|
|
outputs = await run_batch(
|
|
api_url=api_url,
|
|
prompts=prompts,
|
|
output_len=args.output_len,
|
|
max_concurrency=args.max_concurrency,
|
|
pbar=None,
|
|
)
|
|
benchmark_duration = time.perf_counter() - benchmark_start_time
|
|
|
|
failed_outputs = [output for output in outputs if not output.success]
|
|
if failed_outputs:
|
|
print(f"WARNING: {len(failed_outputs)}/{len(outputs)} requests failed")
|
|
for output in failed_outputs[:5]:
|
|
print(f" {output.error[:160]}")
|
|
|
|
metrics, _ = calculate_metrics(
|
|
outputs=outputs,
|
|
dur_s=benchmark_duration,
|
|
tokenizer=tokenizer,
|
|
)
|
|
|
|
if metrics.completed == 0:
|
|
raise RuntimeError("All requests failed for this shared-prefix percentage.")
|
|
|
|
print_benchmark_result(
|
|
metrics=metrics,
|
|
benchmark_duration=benchmark_duration,
|
|
backend=args.backend,
|
|
request_rate=float("inf"),
|
|
max_concurrency=args.max_concurrency,
|
|
)
|
|
|
|
maybe_write_summary_jsonl(
|
|
pct=pct,
|
|
prefix_len=prefix_len,
|
|
suffix_len=suffix_len,
|
|
metrics=metrics,
|
|
output_file=args.output_file,
|
|
benchmark_duration=benchmark_duration,
|
|
)
|
|
|
|
return metrics, benchmark_duration, prefix_len, suffix_len, len(outputs)
|
|
|
|
|
|
async def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"--backend",
|
|
type=str,
|
|
default="sglang",
|
|
choices=["sglang"],
|
|
help="Warm-cache benchmark currently supports the native SGLang /generate endpoint.",
|
|
)
|
|
parser.add_argument(
|
|
"--base-url",
|
|
type=str,
|
|
default=None,
|
|
help="Server base url if not using host and port.",
|
|
)
|
|
parser.add_argument("--host", type=str, default="127.0.0.1")
|
|
parser.add_argument("--port", type=int, default=30000)
|
|
parser.add_argument(
|
|
"--model",
|
|
type=str,
|
|
required=True,
|
|
help="Name or path of the model. Used to load the tokenizer and vocab ids.",
|
|
)
|
|
parser.add_argument(
|
|
"--tokenizer",
|
|
type=str,
|
|
default=None,
|
|
help="Name or path of the tokenizer. Defaults to --model.",
|
|
)
|
|
parser.add_argument(
|
|
"--num-prompts",
|
|
type=int,
|
|
default=64,
|
|
help="Number of prompts to process per shared-prefix percentage.",
|
|
)
|
|
parser.add_argument(
|
|
"--total-tokens",
|
|
type=int,
|
|
default=70000,
|
|
help="Total input tokens per request (shared prefix + unique suffix).",
|
|
)
|
|
parser.add_argument(
|
|
"--output-len",
|
|
type=int,
|
|
default=200,
|
|
help="Output length for each request.",
|
|
)
|
|
parser.add_argument(
|
|
"--max-concurrency",
|
|
type=int,
|
|
default=4,
|
|
help="Maximum number of concurrent requests.",
|
|
)
|
|
parser.add_argument(
|
|
"--pcts",
|
|
type=str,
|
|
default="0,10,20,30,40,50,60,70,80,90,92,95,97,99",
|
|
help="Comma-separated shared-prefix percentages to sweep.",
|
|
)
|
|
parser.add_argument(
|
|
"--seed",
|
|
type=int,
|
|
default=42,
|
|
help="Random seed for synthetic prompt generation.",
|
|
)
|
|
parser.add_argument(
|
|
"--disable-ignore-eos",
|
|
action="store_true",
|
|
help="Disable ignoring EOS.",
|
|
)
|
|
parser.add_argument(
|
|
"--output-file",
|
|
type=str,
|
|
default=None,
|
|
help="Optional JSONL file to append one result object per shared-prefix percentage.",
|
|
)
|
|
parser.add_argument(
|
|
"--extra-request-body",
|
|
metavar='{"key1": "value1", "key2": "value2"}',
|
|
type=str,
|
|
help="Append given JSON object to the request payload. You can use this to specify additional generate params.",
|
|
)
|
|
global args
|
|
args = parser.parse_args()
|
|
|
|
args.extra_request_body = (
|
|
json.loads(args.extra_request_body) if args.extra_request_body else {}
|
|
)
|
|
|
|
base_url = args.base_url or f"http://{args.host}:{args.port}"
|
|
api_url = f"{base_url}/generate"
|
|
pcts = [int(p.strip()) for p in args.pcts.split(",") if p.strip()]
|
|
rng = random.Random(args.seed)
|
|
|
|
tokenizer_id = args.tokenizer if args.tokenizer is not None else args.model
|
|
tokenizer = get_tokenizer(tokenizer_id)
|
|
vocab_ids = list(tokenizer.get_vocab().values())
|
|
|
|
print(f"{args}\n")
|
|
print(f"Loading tokenizer from {tokenizer_id} ...")
|
|
print(f"Tokenizer loaded (vocab_size={len(vocab_ids)})")
|
|
|
|
for pct in pcts:
|
|
await benchmark_shared_prefix_pct(
|
|
api_url=api_url,
|
|
base_url=base_url,
|
|
tokenizer=tokenizer,
|
|
vocab_ids=vocab_ids,
|
|
rng=rng,
|
|
pct=pct,
|
|
)
|
|
|
|
if args.output_file:
|
|
print(f"JSONL results saved to {args.output_file}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
set_ulimit()
|
|
asyncio.run(main())
|