Make bench_one_batch_server compatible for more backends (#18512)

This commit is contained in:
maocheng23
2026-02-10 10:36:39 +08:00
committed by GitHub
parent 4a1b50bb2d
commit 1d366f1206
@@ -109,6 +109,7 @@ class BenchArgs:
append_to_github_summary: bool = True append_to_github_summary: bool = True
seed: int = 42 seed: int = 42
cache_hit_rate: float = 0.0 cache_hit_rate: float = 0.0
backend: str = "sglang"
server_args_for_metrics: Optional[List[str]] = None server_args_for_metrics: Optional[List[str]] = None
@staticmethod @staticmethod
@@ -193,6 +194,13 @@ class BenchArgs:
help="Cache hit rate for benchmarking (0.0-1.0). " help="Cache hit rate for benchmarking (0.0-1.0). "
"0.0 means no cache hits (flush all), 0.4 means 40%% of input tokens are cached.", "0.0 means no cache hits (flush all), 0.4 means 40%% of input tokens are cached.",
) )
parser.add_argument(
"--backend",
type=str,
default=BenchArgs.backend,
choices=["sglang", "vllm"],
help="Backend server type (sglang or vllm).",
)
parser.add_argument( parser.add_argument(
"--server-args-for-metrics", "--server-args-for-metrics",
type=str, type=str,
@@ -289,8 +297,10 @@ def _warmup_cache(
cache_hit_rate: float, cache_hit_rate: float,
dataset_name: str = "random", dataset_name: str = "random",
image_data: Optional[List] = None, image_data: Optional[List] = None,
backend: str = "sglang",
model_name: Optional[str] = None,
): ):
"""Warm up the cache by sending prefix tokens to populate the radix cache. """Warm up the cache by sending prefix tokens to populate the radix/prefix cache.
Args: Args:
url: Server URL url: Server URL
@@ -299,6 +309,8 @@ def _warmup_cache(
cache_hit_rate: Fraction of input tokens to cache (0.0-1.0) cache_hit_rate: Fraction of input tokens to cache (0.0-1.0)
dataset_name: Name of the dataset (used to determine if image data should be included) dataset_name: Name of the dataset (used to determine if image data should be included)
image_data: Optional image data for VLM models image_data: Optional image data for VLM models
backend: Backend server type ("sglang" or "vllm")
model_name: Model name (required for vllm backend)
""" """
cached_token_len = int(input_len * cache_hit_rate) cached_token_len = int(input_len * cache_hit_rate)
if cached_token_len <= 0: if cached_token_len <= 0:
@@ -310,21 +322,33 @@ def _warmup_cache(
) )
# Create prefix input_ids for cache warming # Create prefix input_ids for cache warming
cache_warmup_input_ids = [ids[:cached_token_len] for ids in input_ids] cache_warmup_input_ids = [ids[:cached_token_len] for ids in input_ids]
cache_warmup_payload = {
"input_ids": cache_warmup_input_ids, if backend == "vllm":
"sampling_params": { cache_warmup_payload = {
"model": model_name,
"prompt": cache_warmup_input_ids,
"max_tokens": 1,
"temperature": 0.0, "temperature": 0.0,
"max_new_tokens": 1, # Minimal output, just to populate cache "stream": False,
"ignore_eos": True, }
}, gen_url = url + "/v1/completions"
"stream": False, else:
} cache_warmup_payload = {
if dataset_name == "mmmu" and image_data is not None: "input_ids": cache_warmup_input_ids,
# include image data in cache warmup "sampling_params": {
cache_warmup_payload["image_data"] = image_data "temperature": 0.0,
"max_new_tokens": 1, # Minimal output, just to populate cache
"ignore_eos": True,
},
"stream": False,
}
if dataset_name == "mmmu" and image_data is not None:
# include image data in cache warmup
cache_warmup_payload["image_data"] = image_data
gen_url = url + "/generate"
warmup_response = requests.post( warmup_response = requests.post(
url + "/generate", gen_url,
json=cache_warmup_payload, json=cache_warmup_payload,
timeout=DEFAULT_TIMEOUT, timeout=DEFAULT_TIMEOUT,
) )
@@ -353,9 +377,16 @@ def run_one_case(
dataset_path: str = BenchArgs.dataset_path, dataset_path: str = BenchArgs.dataset_path,
parallel_batch: bool = False, parallel_batch: bool = False,
cache_hit_rate: float = BenchArgs.cache_hit_rate, cache_hit_rate: float = BenchArgs.cache_hit_rate,
backend: str = "sglang",
model_name: Optional[str] = None,
): ):
response = requests.post(url + "/flush_cache", timeout=DEFAULT_TIMEOUT) if backend == "vllm":
response.raise_for_status() # You need to have export VLLM_SERVER_DEV_MODE=1 in your environment to use this endpoint.
response = requests.post(url + "/reset_prefix_cache", timeout=DEFAULT_TIMEOUT)
response.raise_for_status()
else:
response = requests.post(url + "/flush_cache", timeout=DEFAULT_TIMEOUT)
response.raise_for_status()
# Load input token ids # Load input token ids
# TODO: reuse bench_serving.get_dataset ? # TODO: reuse bench_serving.get_dataset ?
@@ -378,45 +409,62 @@ def run_one_case(
return_text=False, return_text=False,
) )
# Load sampling parameters # Extract input_ids from requests
use_structured_outputs = False
if use_structured_outputs:
texts = []
for _ in range(batch_size):
texts.append(
"Human: What is the capital city of france? can you give as many trivial information as possible about that city? answer in json.\n"
* 50
+ "Assistant:"
)
json_schema = "$$ANY$$"
else:
json_schema = None
payload = {
"sampling_params": {
"temperature": temperature,
"max_new_tokens": output_len,
"ignore_eos": True,
"json_schema": json_schema,
"stream_interval": stream_interval,
},
"return_logprob": return_logprob,
"stream": True,
**({"parallel_batch": parallel_batch} if parallel_batch else {}),
}
if dataset_name == "mmmu": if dataset_name == "mmmu":
# vlm
input_ids = [] input_ids = []
# for vlms, tokenizer is an instance of AutoProcessor # for vlms, tokenizer is an instance of AutoProcessor
tokenizer = tokenizer.tokenizer tokenizer = tokenizer.tokenizer
for input_req in input_requests: for input_req in input_requests:
input_ids += [tokenizer.encode(input_req.prompt)] input_ids += [tokenizer.encode(input_req.prompt)]
payload["image_data"] = [req.image_data for req in input_requests] image_data = [req.image_data for req in input_requests]
else: else:
input_ids = [req.prompt for req in input_requests] input_ids = [req.prompt for req in input_requests]
image_data = None
payload["input_ids"] = input_ids # Build payload based on backend
if backend == "vllm":
payload = {
"model": model_name,
"prompt": input_ids,
"max_tokens": output_len,
"temperature": temperature,
"stream": True,
"ignore_eos": True,
}
if return_logprob:
payload["logprobs"] = 1
gen_url = url + "/v1/completions"
else:
# Load sampling parameters
use_structured_outputs = False
if use_structured_outputs:
texts = []
for _ in range(batch_size):
texts.append(
"Human: What is the capital city of france? can you give as many trivial information as possible about that city? answer in json.\n"
* 50
+ "Assistant:"
)
json_schema = "$$ANY$$"
else:
json_schema = None
payload = {
"sampling_params": {
"temperature": temperature,
"max_new_tokens": output_len,
"ignore_eos": True,
"json_schema": json_schema,
"stream_interval": stream_interval,
},
"return_logprob": return_logprob,
"stream": True,
**({"parallel_batch": parallel_batch} if parallel_batch else {}),
}
payload["input_ids"] = input_ids
if image_data is not None:
payload["image_data"] = image_data
gen_url = url + "/generate"
# Warm up cache if cache_hit_rate > 0.0 # Warm up cache if cache_hit_rate > 0.0
if cache_hit_rate > 0.0: if cache_hit_rate > 0.0:
@@ -426,7 +474,9 @@ def run_one_case(
input_len=input_len, input_len=input_len,
cache_hit_rate=cache_hit_rate, cache_hit_rate=cache_hit_rate,
dataset_name=dataset_name, dataset_name=dataset_name,
image_data=payload.get("image_data"), image_data=image_data,
backend=backend,
model_name=model_name,
) )
# Turn on profiler # Turn on profiler
@@ -447,7 +497,7 @@ def run_one_case(
# Run the request # Run the request
tic = time.perf_counter() tic = time.perf_counter()
response = requests.post( response = requests.post(
url + "/generate", gen_url,
json=payload, json=payload,
stream=True, stream=True,
timeout=DEFAULT_TIMEOUT, timeout=DEFAULT_TIMEOUT,
@@ -456,21 +506,40 @@ def run_one_case(
# Get the TTFT of the last request in the batch # Get the TTFT of the last request in the batch
last_ttft = 0.0 last_ttft = 0.0
for chunk in response.iter_lines(decode_unicode=False): if backend == "vllm":
chunk = chunk.decode("utf-8") # Parse OpenAI-compatible streaming format from vLLM
if chunk and chunk.startswith("data:"): first_token_indices = set()
if chunk == "data: [DONE]": for chunk in response.iter_lines(decode_unicode=False):
break chunk = chunk.decode("utf-8")
data = json.loads(chunk[5:].strip("\n")) if chunk and chunk.startswith("data:"):
if "error" in data: data_str = chunk[5:].strip()
raise RuntimeError(f"Request has failed. {data}.") if data_str == "[DONE]":
break
data = json.loads(data_str)
if "error" in data:
raise RuntimeError(f"Request has failed. {data}.")
for choice in data.get("choices", []):
idx = choice["index"]
if idx not in first_token_indices:
first_token_indices.add(idx)
if len(first_token_indices) == batch_size:
last_ttft = time.perf_counter() - tic
else:
for chunk in response.iter_lines(decode_unicode=False):
chunk = chunk.decode("utf-8")
if chunk and chunk.startswith("data:"):
if chunk == "data: [DONE]":
break
data = json.loads(chunk[5:].strip("\n"))
if "error" in data:
raise RuntimeError(f"Request has failed. {data}.")
assert ( assert (
data["meta_info"]["finish_reason"] is None data["meta_info"]["finish_reason"] is None
or data["meta_info"]["finish_reason"]["type"] == "length" or data["meta_info"]["finish_reason"]["type"] == "length"
) )
if data["meta_info"]["completion_tokens"] == 1: if data["meta_info"]["completion_tokens"] == 1:
last_ttft = time.perf_counter() - tic last_ttft = time.perf_counter() - tic
# Compute metrics # Compute metrics
latency = time.perf_counter() - tic latency = time.perf_counter() - tic
@@ -478,12 +547,17 @@ def run_one_case(
output_throughput = batch_size * output_len / (latency - last_ttft) output_throughput = batch_size * output_len / (latency - last_ttft)
overall_throughput = batch_size * (input_len + output_len) / latency overall_throughput = batch_size * (input_len + output_len) / latency
response = requests.get(url + "/get_server_info", timeout=DEFAULT_TIMEOUT) if backend == "vllm":
response.raise_for_status() # vLLM does not expose these metrics via API
server_info = response.json() last_gen_throughput = -1
internal_state = server_info.get("internal_states", [{}]) acc_length = -1
last_gen_throughput = internal_state[0].get("last_gen_throughput", None) or -1 else:
acc_length = internal_state[0].get("avg_spec_accept_length", None) or -1 response = requests.get(url + "/get_server_info", timeout=DEFAULT_TIMEOUT)
response.raise_for_status()
server_info = response.json()
internal_state = server_info.get("internal_states", [{}])
last_gen_throughput = internal_state[0].get("last_gen_throughput", None) or -1
acc_length = internal_state[0].get("avg_spec_accept_length", None) or -1
# Calculate cache hit rate from before/after metrics delta # Calculate cache hit rate from before/after metrics delta
metrics_after = get_cache_tokens_from_metrics(url) metrics_after = get_cache_tokens_from_metrics(url)
@@ -638,35 +712,58 @@ def run_benchmark_internal(
else: else:
proc, base_url = launch_server_process(launch_server_func, server_args) proc, base_url = launch_server_process(launch_server_func, server_args)
# Get tokenizer # Get tokenizer and server info
response = requests.get(base_url + "/get_server_info", timeout=DEFAULT_TIMEOUT) if bench_args.backend == "vllm":
response.raise_for_status() # For vLLM, get model name from /v1/models endpoint
server_info = response.json() print(f"Connecting to vLLM server at {base_url}...")
if "tokenizer_path" in server_info: response = requests.get(base_url + "/v1/models", timeout=DEFAULT_TIMEOUT)
tokenizer_path = server_info["tokenizer_path"] response.raise_for_status()
elif "prefill" in server_info: model_list = response.json().get("data", [])
tokenizer_path = server_info["prefill"][0]["tokenizer_path"] if not model_list:
if bench_args.dataset_name == "mmmu": raise RuntimeError("No models found on vLLM server via /v1/models")
# mmmu implies this is a MLLM model_name = model_list[0]["id"]
tokenizer = get_processor(tokenizer_path) print(f"Found model: {model_name}")
print(f"Loading tokenizer for {model_name}...")
if bench_args.dataset_name == "mmmu":
tokenizer = get_processor(model_name)
else:
tokenizer = get_tokenizer(model_name)
print("Tokenizer loaded.")
server_info = {"model_name": model_name}
# vLLM does not expose token capacity or max running requests via API
skip_token_capacity_threshold = float("inf")
skip_max_running_requests_threshold = float("inf")
else: else:
tokenizer = get_tokenizer(tokenizer_path) model_name = None
response = requests.get(base_url + "/get_server_info", timeout=DEFAULT_TIMEOUT)
response.raise_for_status()
server_info = response.json()
if "tokenizer_path" in server_info:
tokenizer_path = server_info["tokenizer_path"]
elif "prefill" in server_info:
tokenizer_path = server_info["prefill"][0]["tokenizer_path"]
if bench_args.dataset_name == "mmmu":
# mmmu implies this is a MLLM
tokenizer = get_processor(tokenizer_path)
else:
tokenizer = get_tokenizer(tokenizer_path)
# Get token capacity # Get token capacity
internal_state = server_info.get("internal_states", [{}]) internal_state = server_info.get("internal_states", [{}])
skip_token_capacity_threshold = ( skip_token_capacity_threshold = (
internal_state[0].get("memory_usage", {}).get("token_capacity", 1000000000) internal_state[0].get("memory_usage", {}).get("token_capacity", 1000000000)
) )
# Get effective max running requests # Get effective max running requests
max_running_requests_per_dp = internal_state[0].get( max_running_requests_per_dp = internal_state[0].get(
"effective_max_running_requests_per_dp", -1 "effective_max_running_requests_per_dp", -1
) )
dp_size = server_info.get("dp_size", None) or 1 dp_size = server_info.get("dp_size", None) or 1
assert ( assert (
max_running_requests_per_dp > 0 max_running_requests_per_dp > 0
), f"effective_max_running_requests_per_dp is not set, {max_running_requests_per_dp=}" ), f"effective_max_running_requests_per_dp is not set, {max_running_requests_per_dp=}"
skip_max_running_requests_threshold = max_running_requests_per_dp * dp_size skip_max_running_requests_threshold = max_running_requests_per_dp * dp_size
# Warmup # Warmup
if not bench_args.skip_warmup: if not bench_args.skip_warmup:
@@ -688,6 +785,8 @@ def run_benchmark_internal(
dataset_name=bench_args.dataset_name, dataset_name=bench_args.dataset_name,
dataset_path=bench_args.dataset_path, dataset_path=bench_args.dataset_path,
parallel_batch=bench_args.parallel_batch, parallel_batch=bench_args.parallel_batch,
backend=bench_args.backend,
model_name=model_name,
) )
print("=" * 8 + " Warmup End " + "=" * 8 + "\n") print("=" * 8 + " Warmup End " + "=" * 8 + "\n")
@@ -721,6 +820,8 @@ def run_benchmark_internal(
dataset_path=bench_args.dataset_path, dataset_path=bench_args.dataset_path,
parallel_batch=bench_args.parallel_batch, parallel_batch=bench_args.parallel_batch,
cache_hit_rate=bench_args.cache_hit_rate, cache_hit_rate=bench_args.cache_hit_rate,
backend=bench_args.backend,
model_name=model_name,
) )
) )
@@ -761,6 +862,8 @@ def run_benchmark_internal(
profile_by_stage=bench_args.profile_by_stage, profile_by_stage=bench_args.profile_by_stage,
profile_prefix=profile_prefix, profile_prefix=profile_prefix,
profile_output_dir=bench_args.profile_output_dir, profile_output_dir=bench_args.profile_output_dir,
backend=bench_args.backend,
model_name=model_name,
) )
) )
except Exception as e: except Exception as e: