Files
2026-07-13 12:24:33 +08:00

496 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# Adapted from
# https://github.com/vllm-project/vllm/blob/main/benchmarks/benchmark_long_document_qa_throughput.py
"""
Commandline arguments:
--num-total-documents: The number of documents to sample prompts from.
--document-length: The length of each document in tokens.
(Optional, default: 20000)
--output-len: The number of tokens to generate for each prompt.
(Optional, default: 100)
--num-requests: The number of requests to send.
--num-docs-per-request: The number of documents to use in each prompt.
--sampling-strategy: The sampling strategy to use. Currently only supports
"random".
--random-seed: Random seed when the repeat mode is "random".
(Optional, default: 0)
--blend-special-str: The special string to use for blending documents.
(Optional, default: " # # ")
--port: Port to query the vLLM server
--model: Model name
--max-inflight-requests: Maximum number of in-flight requests. Default is 2
--sleep-time-after-warmup: Sleep time after warm up iteration.
(Optional, default: 0.0 seconds)
--output: Filename to write all responses to. If omitted, writes to stdout.
--expected-ttft-gain: Expected minimum speed-up in time-to-first-token
(warmup/query) as a factor, e.g. 4.3 for 4.3×. If
actual gain is below this, exits.
--expected-latency-gain: Expected minimum speed-up in total round time
(warmup/query) as a factor, e.g. 4.5 for 4.5×.
If actual gain is below this, exits.
"""
# Standard
import argparse
import asyncio
import random
import sys
import time
# Third Party
from openai import AsyncOpenAI
from transformers import AutoTokenizer
# Global output filename (set in __main__)
OUTPUT_FILE = None
def has_content(chunk):
"""
Check if the chunk has content in the choices.
Args:
chunk: The response chunk from OpenAI API.
Returns:
bool: True if content exists, False otherwise.
"""
return chunk.choices and chunk.choices[0].text
def extract_content(chunk):
"""
Extract content from the response chunk.
Args:
chunk: The response chunk from OpenAI API.
Returns:
str: The content extracted from the chunk.
"""
if chunk.choices[0].text is not None:
return chunk.choices[0].text
else:
return ""
def write_resp(text: str):
"""
Write text to the specified output file (if any), otherwise to stdout.
"""
if OUTPUT_FILE:
with open(OUTPUT_FILE, "a") as resp_file:
resp_file.write(text)
else:
sys.stdout.write(text)
async def process_single_prompt(
client, model, prompt, prompt_index, total_prompts, output_len, semaphore
):
"""
Process a single prompt with the given client and model.
Args:
client: The OpenAI client for making API calls.
model: The model name to use for generation.
prompt: The prompt string to be processed.
prompt_index: Index of the current prompt (0-based).
total_prompts: Total number of prompts being processed.
output_len: The maximum number of tokens to generate.
semaphore: Asyncio semaphore to limit concurrent requests.
Returns:
float: Time-to-first-token measurement
"""
async with semaphore: # Acquire semaphore to limit concurrent requests
write_resp(f"\n--- Sending prompt {prompt_index + 1}/{total_prompts} ---\n")
start_time = time.time()
first_token_time = None
words = ""
response = await client.completions.create(
model=model,
prompt=prompt,
max_tokens=output_len,
temperature=0.0,
stream=True,
extra_body={"ignore_eos": True},
)
responses = []
# Collect the response chunks
async for chunk in response:
if not chunk.choices:
continue
# Handle content for chat completions
if has_content(chunk):
content = extract_content(chunk)
if first_token_time is None and content != "":
first_token_time = time.time()
responses.append(content)
words += content
final_response = "".join(responses)
write_resp(f"\nResponse of request {prompt_index}: {final_response}\n")
if first_token_time is not None:
return first_token_time - start_time
else:
# If no content was generated, return a default value
return 0.0
async def test_long_document_qa(
client, model, prompts=None, output_len=100, max_inflight_requests=10
):
"""
Test long document QA with the given prompts and sampling parameters.
Process prompts concurrently with a limit on inflight requests.
Args:
client: The OpenAI client for making API calls.
model: The model name to use for generation.
prompts: A list of prompt strings to be processed by the LLM.
output_len: The maximum number of tokens to generate.
max_inflight_requests: Maximum number of concurrent requests.
Returns:
list: ttfts - a list of time-to-first-token measurements
"""
# Create semaphore to limit concurrent requests
semaphore = asyncio.Semaphore(max_inflight_requests)
# Create tasks for all prompts
tasks = []
for i, prompt in enumerate(prompts):
task = process_single_prompt(
client=client,
model=model,
prompt=prompt,
prompt_index=i,
total_prompts=len(prompts),
output_len=output_len,
semaphore=semaphore,
)
tasks.append(task)
# Execute all tasks concurrently and collect results
ttfts = await asyncio.gather(*tasks)
return ttfts
def generate_warmup_prompt_ids(
doc_prompts, sys_prompts, query_prompts, blend_special_str, tokenizer, offset=1
):
blend_special_ids = tokenizer.encode(blend_special_str)[offset:]
warmup_prompt_ids = []
for doc_prompt, sys_prompt, query_prompt in zip(
doc_prompts, sys_prompts, query_prompts, strict=False
):
sys_prompt_ids = tokenizer.encode(sys_prompt)
doc_prompt_ids = tokenizer.encode(doc_prompt)[offset:]
query_prompt_ids = tokenizer.encode(query_prompt)[offset:]
warmup_prompt_ids.append(
sys_prompt_ids
+ blend_special_ids
+ doc_prompt_ids
+ blend_special_ids
+ query_prompt_ids
)
return warmup_prompt_ids
def generate_prompt_ids(
doc_prompts: list[str],
sys_prompts: list[str],
query_prompts: list[str],
num_requests: int,
num_docs_per_request: int,
blend_special_str: str,
tokenizer,
offset: int = 1,
):
blend_special_ids = tokenizer.encode(blend_special_str)[offset:]
prompt_ids = []
for i in range(num_requests):
temp_prompt_ids = []
sample_docs = random.sample(doc_prompts, num_docs_per_request)
sample_docs_ids = [tokenizer.encode(doc)[offset:] for doc in sample_docs]
sys_prompt_ids = tokenizer.encode(sys_prompts[i])
query_prompt_ids = tokenizer.encode(query_prompts[i])[offset:]
temp_prompt_ids += sys_prompt_ids
for doc_ids in sample_docs_ids:
temp_prompt_ids += blend_special_ids + doc_ids
temp_prompt_ids += blend_special_ids + query_prompt_ids
prompt_ids.append(temp_prompt_ids)
return prompt_ids
async def main(args):
random.seed(args.random_seed)
# Create the OpenAI client
client = AsyncOpenAI(
base_url=f"http://localhost:{args.port}/v1", api_key="sk-dummy"
)
model = args.model
blend_special_str = args.blend_special_str
num_requests = args.num_requests
num_docs_per_request = args.num_docs_per_request
document_length = args.document_length
num_total_documents = args.num_total_documents
tokenizer = AutoTokenizer.from_pretrained(args.model)
doc_prompts = [
str(i) + " " + " ".join(["hi"] * document_length)
for i in range(num_total_documents)
]
warmup_sys_prompts = ["You are a helpful assistant."] * num_total_documents
warmup_query_prompts = ["What's up? how are you recently?"] * num_total_documents
warmup_prompt_ids = generate_warmup_prompt_ids(
doc_prompts,
warmup_sys_prompts,
warmup_query_prompts,
blend_special_str,
tokenizer,
offset=1,
)
sys_prompts = ["You are a helpful assistant."] * num_requests
query_prompts = ["What's up? how are you recently?"] * num_requests
prompt_ids = generate_prompt_ids(
doc_prompts,
sys_prompts,
query_prompts,
num_requests,
num_docs_per_request,
blend_special_str,
tokenizer,
offset=1,
)
write_resp("------warm up round------\n")
warmup_start_time = time.time()
warmup_ttfts = await test_long_document_qa(
client=client,
model=model,
prompts=warmup_prompt_ids,
output_len=args.output_len,
max_inflight_requests=args.max_inflight_requests,
)
warmup_end_time = time.time()
write_resp("------query round------\n")
sleep_time_after_warmup = args.sleep_time_after_warmup
if sleep_time_after_warmup > 0:
write_resp(f"Sleeping for {sleep_time_after_warmup} seconds after warmup...\n")
time.sleep(sleep_time_after_warmup)
benchmark_start_time = time.time()
benchmark_ttfts = await test_long_document_qa(
client=client,
model=model,
prompts=prompt_ids,
output_len=args.output_len,
max_inflight_requests=args.max_inflight_requests,
)
benchmark_end_time = time.time()
# Print results
warmup_mean_ttft = sum(warmup_ttfts) / len(warmup_ttfts)
query_mean_ttft = sum(benchmark_ttfts) / len(benchmark_ttfts)
CSI = "\x1b["
RESET = CSI + "0m"
print(f"{CSI}36;1m\n=== BENCHMARK RESULTS ==={RESET}")
print(f"{CSI}32mWarmup round mean TTFT: {warmup_mean_ttft:.3f}s{RESET}")
print(
f"{CSI}33mWarmup round time: {warmup_end_time - warmup_start_time:.3f}s{RESET}"
)
print(f"{CSI}35mWarmup round prompt count: {len(warmup_ttfts)}{RESET}")
print(f"{CSI}32mQuery round mean TTFT: {query_mean_ttft:.3f}s{RESET}")
print(
f"{CSI}33mQuery round time: "
f"{benchmark_end_time - benchmark_start_time:.3f}s{RESET}"
)
print(f"{CSI}35mQuery round prompt count: {len(benchmark_ttfts)}{RESET}")
# Validate expected gains as multiplicative speed-ups
if args.expected_ttft_gain is not None:
actual_ttft_gain = (
warmup_mean_ttft / query_mean_ttft if query_mean_ttft > 0 else float("inf")
)
print(f"{CSI}34mActual TTFT gain: {actual_ttft_gain:.2f}×{RESET}")
if actual_ttft_gain < args.expected_ttft_gain:
sys.exit(
f"ERROR: TTFT gain {actual_ttft_gain:.2f}× < expected "
f"{args.expected_ttft_gain:.2f}×"
)
if args.expected_latency_gain is not None:
warmup_duration = warmup_end_time - warmup_start_time
query_duration = benchmark_end_time - benchmark_start_time
# compute per-prompt latency before comparing
warmup_per_prompt = warmup_duration / len(warmup_ttfts)
query_per_prompt = query_duration / len(benchmark_ttfts)
actual_latency_gain = (
warmup_per_prompt / query_per_prompt
if query_per_prompt > 0
else float("inf")
)
print(f"{CSI}34mActual latency gain: {actual_latency_gain:.2f}×{RESET}")
if actual_latency_gain < args.expected_latency_gain:
sys.exit(
f"ERROR: latency gain {actual_latency_gain:.2f}× < expected "
f"{args.expected_latency_gain:.2f}×"
)
def create_argument_parser():
parser = argparse.ArgumentParser(
description="Benchmark the performance forMulti-Doc QA."
)
parser.add_argument(
"--document-length",
type=int,
# Roughly the number of tokens for a system paper,
# excluding images
default=3000,
help="Length of each document in tokens.",
)
parser.add_argument(
"--num-total-documents",
type=int,
default=100,
help="Number of documents to generate for testing.",
)
parser.add_argument(
"--output-len",
type=int,
default=10,
help="Maximum number of tokens to generate for each prompt.",
)
parser.add_argument(
"--num-requests",
type=int,
default=100,
help="Number of requests to send.",
)
parser.add_argument(
"--num-docs-per-request",
type=int,
default=5,
help="Number of requests to send.",
)
parser.add_argument(
"--sampling-strategy",
type=str,
default="random",
help="Random seed for sampling",
)
parser.add_argument(
"--random-seed",
type=int,
default=0,
help='Random seed when the repeat mode is "random"',
)
parser.add_argument(
"--blend-special-str",
type=str,
default=" # # ",
help="Special string to separate different documents.",
)
parser.add_argument(
"--port",
type=int,
default=8000,
help="Port to query the vLLM server",
)
parser.add_argument(
"--model",
type=str,
default="meta-llama/Llama-3.1-8B-Instruct",
help="Model name",
)
parser.add_argument(
"--max-inflight-requests",
type=int,
default=20,
help="Maximum number of concurrent inflight requests",
)
parser.add_argument(
"--sleep-time-after-warmup",
type=float,
default=0.0,
help="Sleep time after warm up iteration",
)
parser.add_argument(
"--output",
type=str,
default=None,
help="Filename to write all responses to; if omitted, writes to stdout.",
)
parser.add_argument(
"--expected-ttft-gain",
type=float,
default=None,
help=(
"Expected minimum speed-up in time-to-first-token (warmup/query) "
"as a factor, e.g. 4.3 for 4.3×. If actual gain is below this, exits."
),
)
parser.add_argument(
"--expected-latency-gain",
type=float,
default=None,
help=(
"Expected minimum speed-up in total round time (warmup/query) "
"as a factor, e.g. 4.5 for 4.5×. If actual gain is below this, exits."
),
)
return parser
if __name__ == "__main__":
parser = create_argument_parser()
args = parser.parse_args()
OUTPUT_FILE = args.output
asyncio.run(main(args))