项目文件夹

文件
2026-07-13 12:24:33 +08:00

496 行
15 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# 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))