evalstone/evalscope/examples/perf/benchmark_random_request_generation.py
2026-07-08 08:57:50 +00:00

214 lines
6.2 KiB
Python

"""Benchmark random dataset request generation without sending HTTP requests.
This script exercises the same ``get_requests()`` path used by ``evalscope perf``
and measures only local request construction. It is intended for comparing
serial generation (``--num-workers 1``) with multiprocessing generation for the
``random`` dataset.
Example:
python examples/perf/benchmark_random_request_generation.py \
--tokenizer-path Qwen/Qwen2.5-0.5B-Instruct \
--numbers 128 512 \
--prompt-lengths 2048 8192 \
--workers 1 2 4 8 0
"""
import argparse
import asyncio
import time
from dataclasses import dataclass
from typing import Any, Dict, List
from evalscope.perf.arguments import Arguments
from evalscope.perf.benchmark import get_requests
from evalscope.perf.plugin.datasets.random_dataset import RandomDatasetPlugin
from evalscope.perf.utils.worker_util import resolve_dataset_generation_workers
_BENCHMARK_URL = 'http://127.0.0.1:8000/v1/chat/completions'
class _NoopApiPlugin:
"""Build request dictionaries without touching the network."""
def build_request(self, messages: Any) -> Dict[str, Any]:
return {'messages': messages}
@dataclass
class _BenchmarkResult:
number: int
prompt_length: int
configured_workers: int
resolved_workers: int
repeat: int
seconds: float
requests_per_second: float
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description='Benchmark random request generation throughput.')
parser.add_argument(
'--tokenizer-path',
type=str,
default='Qwen/Qwen2.5-0.5B-Instruct',
help='Tokenizer path or model id used by the random dataset.',
)
parser.add_argument(
'--numbers',
type=int,
nargs='+',
default=[128, 512],
help='Request counts to benchmark.',
)
parser.add_argument(
'--prompt-lengths',
type=int,
nargs='+',
default=[2048, 8192],
help='Fixed prompt lengths to benchmark.',
)
parser.add_argument(
'--workers',
type=int,
nargs='+',
default=[1, 2, 4, 8, 0],
help='Configured --num-workers values. Use 0 for auto.',
)
parser.add_argument(
'--repeat',
type=int,
default=1,
help='Number of repeats per case.',
)
parser.add_argument(
'--apply-chat-template',
action='store_true',
default=False,
help='Generate chat-template-shaped messages instead of raw text prompts.',
)
parser.add_argument(
'--tokenize-prompt',
action='store_true',
default=False,
help='Benchmark token-id prompt generation for the completions endpoint.',
)
return parser.parse_args()
def _make_arguments(
tokenizer_path: str,
number: int,
prompt_length: int,
workers: int,
apply_chat_template: bool,
tokenize_prompt: bool,
) -> Arguments:
return Arguments(
model='request-generation-benchmark',
url=_BENCHMARK_URL,
dataset='random',
tokenizer_path=tokenizer_path,
number=number,
parallel=1,
num_workers=workers,
min_prompt_length=prompt_length,
max_prompt_length=prompt_length,
max_tokens=1,
apply_chat_template=apply_chat_template,
tokenize_prompt=tokenize_prompt,
)
def _random_dataset_supports_parallel(args: Arguments) -> bool:
plugin = object.__new__(RandomDatasetPlugin)
plugin.query_parameters = args
plugin.number = args.total_count
return plugin.supports_parallel_message_generation(args.total_count)
async def _consume_requests(args: Arguments) -> int:
count = 0
async for _request, _is_warmup in get_requests(args, _NoopApiPlugin()):
count += 1
return count
def _run_case(
tokenizer_path: str,
number: int,
prompt_length: int,
workers: int,
repeat: int,
apply_chat_template: bool,
tokenize_prompt: bool,
) -> _BenchmarkResult:
args = _make_arguments(
tokenizer_path=tokenizer_path,
number=number,
prompt_length=prompt_length,
workers=workers,
apply_chat_template=apply_chat_template,
tokenize_prompt=tokenize_prompt,
)
resolved_workers = resolve_dataset_generation_workers(
args=args,
total_count=args.total_count,
supports_parallel_generation=_random_dataset_supports_parallel(args),
)
start = time.perf_counter()
count = asyncio.run(_consume_requests(args))
seconds = time.perf_counter() - start
if count != args.total_count:
raise RuntimeError(f'Expected {args.total_count} requests, got {count}.')
return _BenchmarkResult(
number=number,
prompt_length=prompt_length,
configured_workers=workers,
resolved_workers=resolved_workers,
repeat=repeat,
seconds=seconds,
requests_per_second=count / seconds,
)
def _run_matrix(args: argparse.Namespace) -> List[_BenchmarkResult]:
results = []
for number in args.numbers:
for prompt_length in args.prompt_lengths:
for workers in args.workers:
for repeat_index in range(args.repeat):
results.append(
_run_case(
tokenizer_path=args.tokenizer_path,
number=number,
prompt_length=prompt_length,
workers=workers,
repeat=repeat_index + 1,
apply_chat_template=args.apply_chat_template,
tokenize_prompt=args.tokenize_prompt,
)
)
return results
def _print_results(results: List[_BenchmarkResult]) -> None:
print('number,prompt_length,configured_workers,resolved_workers,repeat,seconds,requests_per_second')
for result in results:
print(
f'{result.number},{result.prompt_length},{result.configured_workers},'
f'{result.resolved_workers},{result.repeat},{result.seconds:.3f},'
f'{result.requests_per_second:.2f}'
)
def main() -> None:
args = _parse_args()
results = _run_matrix(args)
_print_results(results)
if __name__ == '__main__':
main()